View
677
Download
3
Category
Preview:
Citation preview
まっくろわーるどじゅりあらんぐッ!
yomichi
JuliaTokyo #3 2015/04/25
スライドとサンプルコードhttps://github.com/yomichi/JuliaTokyo3
1 / 38
自己紹介
HN : 夜道, yomichi
twitter : @yomichi_137
ぽすどくにねんせい統計物理学・計算物理学
主にモンテカルロ法とか言語は C++, Python, Julia
イベント(勉強会)実況勢最近だと全ゲ連(同人ゲーム開発)とか、PyConJP (Python) とか、JuliaTokyoとか
Julia nightly build 勢
Julia の開発環境は REPL と Vim
コミケで Julia 本出したりしてますblog とか締め切りがないので書けません><抽選受かっていたら次の夏にも出します
多分今回の話をもう少し詳しく書きます
(thanks @am_11)
2 / 38
今日のお話
1 Symbol 型と Expr 型 – Julia の抽象構文木2 マクロシステム3 メタプログラミング – どう使うか
メタプログラミングとかマクロ展開とかのお話です(Julia の中では)難しそう or 難しいためか余り触れられてこない話確実に 30分では終わらないので適当に飛ばします
一応スライドだけでも読めるつもりで作った 1 ので、興味ある方はあとでゆっくりとどうぞいつものことですが公式ドキュメントが一番詳しくてわかりやすく、ほぼ常に最新なので、英語が苦じゃない人はそちらがおすすめ
未確認で進行形ネタを仕込もうかと思ったけれどそんな余裕がありませんでした
なのでタイトルは出オチです1だから詰め込みすぎになった、とも言う
3 / 38
Outline
1 Symbol 型と Expr 型 – Julia の抽象構文木
2 マクロシステム
3 メタプログラミング – どう使うか
4 / 38
Julia の抽象構文木 (AST)
例えば x + (y + z) という式は Juliaの中の人からは右図のように木構造(抽象構文木)として見えている
:call は関数呼び出しの意味+ は中置が認められているというだけで普通の関数であることに注意つまり正確には +(x, +(y, z))
Julia は式をこのように抽象構文木に変換して、それから式の評価をしている
この構造をそのまま保持することで、式やプログラムそのものをデータとして扱える(=同図像性)プログラムを生成するプログラムも書ける(=メタプログラミング)
:call
:call:+
:+
:x
:y :z
5 / 38
Julia の抽象構文木 (AST) と Symbol型、 Expr型
Julia における最も重要な型(データ構造)のうち 2つがSymbol と Expr である
Symbol は識別子・変数名を表す型Expr は Julia そのものの抽象構文木(AST)を表す型
これらの型のオブジェクト(シンボル、AST)は:()やquote ... end を用いることで作ることができる
1 julia > s = :x2 :x34 julia > typeof(s)5 Symbol67 julia > ex = :(x + 1)8 :(x + 1)9
10 julia > typeof(ex)11 Expr
6 / 38
Julia の抽象構文木 (AST) と Symbol型、 Expr型
Expr は head, args, typ の 3つの field を持つ1 head は行う操作2 args は操作の対象(引数)3 typ は結果の型(確定していれば)
ほとんどの場合で Any であり、気にする必要はほとんど無い
Base.dump でまとめて表示できるBase.Meta.show_sexpr を使うと S式で表示できる
1 julia > dump(ex)2 Expr3 head: Symbol call4 args: Array(Any ,(3 ,))5 1: Symbol +6 2: Symbol x7 3: Int64 18 typ: Any9
10 julia > Base.Meta.show_sexpr(ex)11 (:call , :+, :x, 1)
7 / 38
Julia の抽象構文木 (AST) と Symbol型、 Expr型
AST は木構造なので節と葉を持つ節は Expr 型葉は Symbol 型の値(変数名)かリテラル(数値型・文字列型)今回、簡単のため Expr, Symbol, リテラルをすべてまとめてAST と呼ぶ
1 julia > dump( :( x + ( y + z ) ) )2 Expr3 head: Symbol call4 args: Array(Any ,(3 ,))5 1: Symbol +6 2: Symbol x7 3: Expr8 head: Symbol call9 args: Array(Any ,(3,))
10 1: Symbol +11 2: Symbol y12 3: Symbol z13 typ: Any14 typ: Any
8 / 38
変数補間 (interpolation)
quote で AST を作るときに、$ を使うことで変数や式の値を入れることができる
文字列 (" ")やプロセス (‘ ‘)における補間と同じ
1 julia > y = 422 4234 julia > :(x = y)5 :(x = y)67 julia > :(x = $y)8 :(x = 42)9
10 julia > :(x = sin (1.0))11 :(x = sin (1.0))1213 julia > :(x = $(sin (1.0)))14 :(x = 0.8414709848078965)
9 / 38
変数補間で Symbol を陽に残す
例えば Symbol を受け取る関数を呼び出す式を quote したいときに、その Symbol を変数補間で与えたいこの時そのまま$s と書くと、Symbol ではなく名前が書き込まれてしまう
ほとんどのマクロではこの挙動の方が都合がいい配列かタプルに隠して埋め込み、後から取り出せばよい
1 julia > :( foo(:a) ) # こ れ が 欲 し い2 :(foo(:a))34 julia > s = :a ;5 julia > :( foo($s) ) # 直 接 埋 め 込 む と ダ メ6 :(foo(a))7
8 julia > :( foo( $[s]...) ) # 一 度 配 列 に 隠 す9 :(foo([:a]...))
10
11 julia > :( foo( $(s ,)...) ) # タ プ ル で も 良 い12 :(foo((:a ,)...))
10 / 38
式の評価
eval 関数に AST を渡すことで、AST の評価を行える
AST に未定義な変数を含めることができるが、定義する前にAST を評価するともちろんエラーが出る
1 julia > x2 ERROR: UndefVarError: x not defined34 julia > ex = :(2 * x)5 :(2x)67 julia > eval(ex)8 ERROR: UndefVarError: x not defined9
10 julia > x = 4211 421213 julia > eval(ex)14 84
11 / 38
式の操作
Expr は immutable ではないので、AST を操作することができる
1 julia > ex2 :(2x)34 julia > x, eval(ex)5 (42 ,84)67 julia > ex.args [2] = 20;89 julia > ex
10 :(20x)1112 julia > x, eval(ex)13 (42 ,840)
12 / 38
第一級オブジェクトとしての AST– 同図像性 (Homoiconic)
ソースコードを quote することで AST を生み出すことができた
Expr のコンストラクタを呼び出して作ることもできるparse 関数を使うことで、文字列から作ることもできる
1 julia > parse("x+1")2 :(x + 1)
もちろん関数に渡したり関数から受け取ったりできる既に eval や dump、parse といった実例を見てきたAST を渡すと別の AST に変換する関数を作ることもできる
eval に渡すことでいつでも AST を評価・実行できるこのように、自分自身の AST そのものをデータとして扱える言語の性質を homoiconic と呼ぶ
AST(プログラム)を自動生成するプログラムを簡単に書ける– メタプログラミングマクロを使うことで、より自然な構文の書き換えを行うことができる
13 / 38
Outline
1 Symbol 型と Expr 型 – Julia の抽象構文木
2 マクロシステム
3 メタプログラミング – どう使うか
14 / 38
マクロ
マクロは macro キーワードで定義して、@name という形で呼び出すマクロがやること
1 引数をそれぞれ:() で quote して2 普通の関数と同様に何か仕事して3 返ってきた値を eval する
AST を受け取って AST を返す関数の場合、いちいち quoteや eval をする必要があった
eval( foo( :( x+1 ) ) )
マクロでは @foo(x+1) と書ける@foo x+1 のようにも書ける
マクロは関数と違って健全であるという特徴もある説明は次頁
15 / 38
変数捕捉と健全な (hygienic)マクロ
マクロは ASTを変換(マクロ展開)して、新しい ASTを呼び出し元に貼り付ける展開された ASTに含まれる名前が、呼び出し元の文脈にあるものと衝突する事がある(=変数捕捉)
1 必要な名前が呼び出し元で別の値に束縛されている事がある2 呼び出し元の変数を再束縛してしまう
名前の付け方に細工をすることで回避する1 マクロが定義されているモジュール名を使って名前を修飾する2 変数に値を代入するときは、重複しない(今まで作られていなくて、これからも作られない)名前を生成して変数名とする
Base.gensym で生成可能
Julia のマクロ展開では全て自動でやってくれる(健全なマクロ)
16 / 38
変数捕捉と健全なマクロ
Base.macroexpand を使うとマクロ展開した結果を得ることができる
関数なので quote が必要
1 module JT3 # 以 下 全 て の マ ク ロ は こ の モ ジ ュ ール 内 に あ る
2 macro setx_A ()3 :( x = sin (1.0) )4 end5 end
1 julia > macroexpand( :( JT3.@setx_A ) )2 :(#30#x = JT3.sin (1.0))
1 sin を JT3 で修飾することで、呼び出し元が sin を隠蔽しているかどうかを気にしなくてよくなる
JT3.sin は(名前の隠蔽をしていなければ)Base.sin になる2 #30#x という名前を作ることで、呼び出し元に x があるかどうかを気にしなくてよくなる
17 / 38
変数捕捉と健全なマクロ
1 macro setx_A ()2 :( x = sin (1.0) )3 end
Julia が自動的に x を保護してくれるために、残念ながらこのマクロを使っても呼び出し元の x に変化はおきない
1 julia > x2 ERROR: UndefVarError: x not defined34 julia > JT3.@setx_A5 0.841470984807896567 julia > x8 ERROR: UndefVarError: x not defined
呼び出し元に影響をあたえるためには、名前の保護を無効化することで意図的に変数捕捉を起こす必要がある (Base.esc)
18 / 38
意図的な変数捕捉
Symbol や Expr に Base.esc を作用させると、それらに含まれる名前は保護されなくなる
@setx_B のようにまとめてエスケープしてもよいし@setx_C のように個別にエスケープしてもよい
Base.esc が引数にとれるのは Symbol か Expr だけなので:x
と quote が必要esc(:x) の結果を埋め込むために$() が必要
1 macro setx_B ()2 esc( :( x = sin (1.0)) )3 end4 macro setx_C ()5 :( $(esc(:x)) = sin (1.0) )6 end
1 julia > macroexpand (:( JT3.@setx_B ) )2 :(x = sin (1.0))34 julia > macroexpand (:( JT3.@setx_C ) )5 :(x = JT3.sin (1.0))
19 / 38
意図的な変数捕捉
このマクロを使うと x の値を変えることができる
1 julia > x2 ERROR: UndefVarError: x not defined34 julia > JT3.@setx_B5 0.841470984807896567 julia > x8 0.84147098480789659
10 julia > x = 0;1112 julia > JT3.@setx_C13 0.84147098480789651415 julia > x16 0.8414709848078965
20 / 38
潔癖症レベルで健全
マクロ引数に含まれている変数名も全て保護されるつまり呼び出し元とは関係ないものとなるまず確実に esc が必要
Symbol のまま残したいときはエスケープしなくても良い
ぶっちゃけ迷惑
1 macro setf_A(ex, val)2 :( $ex = $val )3 end
1 julia > macroexpand (:(JT3.@setf_A x y) )2 :(#8#x = JT3.y)34 julia > x, y = 0, 42;56 julia > JT3.@setf_A x y7 ERROR: UndefVarError: y not defined
21 / 38
回避例
基本的にやることはさっきと同じ今回の@setf_C のように、あらかじめ外でエスケープしてから quote 文に注入することもできる
自分の好みで、見やすいものを使ってください
1 macro setf_B(ex, val)2 esc (:( $ex = $val))3 end45 macro setf_C(ex, val)6 esc_ex = esc(ex)7 esc_val = esc(val)8 :( $esc_ex = $esc_val)9 end
1 julia > macroexpand (:( JT3.@setf_B x y))2 :(x = y)
22 / 38
近未来
この「健全すぎる」という件は Julia-0.4 のリリースまでには修正される予定
マクロに渡された式中の名前が保護されなくなる(あまりないと思うけれど)保護したくなったら自分で gensym
やモジュール名修飾すること
Base.@hygienic に関数定義を食わせるとその関数でも名前保護が行われるIssue #6910, #10940
2015-04-25 現在では merge されていないので注意moon/hygienic-macros branch をビルドすれば試せる
1 macro setf_A(ex, val)2 :( $ex = $val )3 end
1 julia -future > macroexpand (:(JT3.@setf_A x y))
2 :(x = y)
23 / 38
近未来
もちろん直に書いた名前は今までどおり保護されることとなるSymbol は$:x とすることでお手軽にエスケープできる
1 macro setx_A ()2 :( x = sin (1.0) )3 end4 macro setx_D ()5 :( $:x = sin (1.0) )6 end
1 julia -future > macroexpand (:(JT3.@setx_A ))2 :(#3#x = JT3.sin (1.0))3 julia -future > macroexpand (:(JT3.@setx_D ))4 :(x = JT3.sin (1.0))
24 / 38
近未来
従来の Base.esc も当然使えるけれど、バグっている?改めて議論の流れを確認してから報告します
1 macro setx_B ()2 esc( :( x = sin (1.0)) )3 end45 macro setx_C ()6 :( $(esc(:x)) = sin (1.0) )7 end
1 julia -future > macroexpand (:(JT3.@setx_B ))2 :(#4#x = JT3.sin (1.0))34 julia -future > macroexpand (:(JT3.@setx_B ))5 :(x = JT3.sin (1.0))
25 / 38
Outline
1 Symbol 型と Expr 型 – Julia の抽象構文木
2 マクロシステム
3 メタプログラミング – どう使うか
26 / 38
マクロの効用 – 評価タイミングの制御
マクロ呼出しでは式を式のまま、値に評価せずに渡すので、評価タイミングを自分で制御できる評価しなかったり、複数回評価したりも可能
与えた式の実行にかかる時間を計測する@time では、現在時刻を調べる操作を前後にやる必要があるExpr や無名関数にくるむことで関数でもほぼ同じことができるが、呼び出し側がいちいちそれをやるのは面倒臭すぎる
1 macro time(ex)2 quote3 t0 = time_ns ()4 val = $(esc(ex))5 t1 = time_ns ()6 println (1.e-9(t1-t0), "␣sec")7 val8 end9 end
27 / 38
マクロの効用 – コンパイル時計算
マクロ展開は式のパース及び関数の JITコンパイル時に行われて、構文そのものが書き換わるそのため、定数などをコンパイル時に計算をおこなうことで実行時間を短くすることができうる
1 function isnotcomment(line)2 !ismatch(r"^\s*(#|$)", line)3 end4 function isnotcomment_nomacro(line)5 !ismatch(Regex("^\\s*(#|\$)"), line)6 end
文字列リテラルの直前に文字を置くと、自動的にマクロ呼出しになる
@r_str は正規表現を作るマクロ
1 julia > foo"hoge"2 ERROR: UndefVarError: @foo_str not defined
28 / 38
マクロの効用 – コンパイル時計算
マクロを使わないと毎回正規表現を作ることになるcode_llvm 関数を使って LLVM コードにコンパイルすると、非マクロ版の方が明らかに中身が多いことが分かる
マクロ版
1 julia > code_llvm(JT3.isnotcomment , (ASCIIString ,))
23 define i1 @julia_isnotcomment_44329 (%
jl_value_t *) {4 top:5 %1 = call i1 @julia_ismatch4396 (%
jl_value_t* inttoptr (i64 4591287408 to%jl_value_t *), %jl_value_t* %0, i64 0)
6 %2 = xor i1 %1, true7 ret i1 %28 }
29 / 38
マクロの効用 – コンパイル時計算
非マクロ版
1 julia > code_llvm(JT3.isnotcomment_nomacro , (ASCIIString ,))
23 define i1 @julia_isnotcomment_nomacro_44330
(% jl_value_t *) {4 top:5 %1 = alloca [3 x %jl_value_t *], align 86 %.sub = getelementptr inbounds [3 x %
jl_value_t *]* %1, i64 0, i64 07 %2 = getelementptr [3 x %jl_value_t *]* %1,
i64 0, i64 28 store %jl_value_t* inttoptr (i64 2 to %
jl_value_t *), %jl_value_t ** %.sub ,align 8
9 %3 = load %jl_value_t *** @jl_pgcstack ,align 8
10 %4 = getelementptr [3 x %jl_value_t *]* %1,i64 0, i64 1
11 %.c = bitcast %jl_value_t ** %3 to %jl_value_t*
12 store %jl_value_t* %.c, %jl_value_t ** %4,align 8
13 store %jl_value_t ** %.sub , %jl_value_t ***@jl_pgcstack , align 8
14 store %jl_value_t* null , %jl_value_t ** %2,align 8
15 %5 = load %jl_value_t ** inttoptr (i644580071440 to %jl_value_t **), align 16
16 %6 = load %jl_value_t ** inttoptr (i644588059808 to %jl_value_t **), align 32
17 %7 = bitcast %jl_value_t* %6 to i32*18 %8 = load i32* %7, align 819 %9 = call %jl_value_t* @julia_call1392 (%
jl_value_t* %5, %jl_value_t* inttoptr (i64 4600855376 to %jl_value_t *), i32%8)
20 store %jl_value_t* %9, %jl_value_t ** %2,align 8
21 %10 = call i1 @julia_ismatch4396 (%jl_value_t* %9, %jl_value_t* %0, i64 0)
22 %11 = xor i1 %10, true23 %12 = load %jl_value_t ** %4, align 824 %13 = getelementptr inbounds %jl_value_t*
%12, i64 0, i32 025 store %jl_value_t ** %13, %jl_value_t ***
@jl_pgcstack , align 826 ret i1 %1127 }
30 / 38
Generated function
4/21 に新しく Base.@generated が導入されたドキュメントも同時に追加された
これまでに渡されたことのない型の組み合わせの時は、関数本体を実行する
その時、引数の値は参照できず、自動的に型が得られる同じ型の組み合わせを再度投げると、本体は実行されず返り値のみが得られる
AST を返すようにすることで、マクロのように使うことができるマクロと違い型による多重ディスパッチが働くことが利点関数本体の処理内容によっては、2回目以降も実行されることがあるらしい
引数の型別にコンパイル時計算が可能パラメタライズ型の型パラメータに整数などを渡せて、違う整数では違う型になることを利用すると、数値ごとにコンパイル時計算しておくことができる
31 / 38
Generated function
百聞は一見にしかずIntを 2回目以降渡した時には println(x) が実行されないこと、println(x) で値ではなく型が出力されていることに注目
1 @generated function gen_fn(x)2 println(x)3 :(x*x)4 end
1 julia > JT3.gen_fn (3)2 Int643 945 julia > JT3.gen_fn (3)6 978 julia > JT3.gen_fn (5)9 25
1011 julia > JT3.gen_fn (3.14)12 Float6413 9.8596
32 / 38
Generated function – コンパイル時フィボナッチ
generated function では再計算しないので、再帰でやっても十分速い
1 type IntTag{N} end23 @generated function fib{N}(:: IntTag{N})4 N < 3 && return 15 ret = fib(N-1) + fib(N-2)6 :($ret)7 end89 fib(N:: Integer) = fib(IntTag{N}())
1 julia > @time JT3.fib (500)2 elapsed time: 0.714843811 seconds (13 MB
allocated)3 48597887408674544024 # 実 は オ ー バ ー フ ロ ー し て い る ( ぉ56 julia > @time JT3.fib (500)7 elapsed time: 1.2101e-5 seconds (192 bytes
allocated)8 4859788740867454402
33 / 38
Generated function – コンパイル時フィボナッチ
再計算していないことは @show ret とかやるとよくわかるあくまでデモ用なので、フィボナッチ数列を作りたい場合は配列を用意してループを回したほうがよい
BigNum などが使えないのでオーバーフローなどに注意再帰が深くなるとスタックオーバーフローする
1 @generated function fib{N}(:: IntTag{N})2 N < 3 && return 13 ret = fib(N-1) + fib(N-2)4 @show N, ret5 :($ret)
1 julia > JT3.fib (100);2 (N,ret) = (3,2)3 (N,ret) = (4,3)4 (N,ret) = (5,5)5 (N,ret) = (6,8)6 # 中 略7 (N,ret) = (98 ,6174643828739884737)8 (N,ret) = (99 , -2437933049959450366) # Oops9 (N,ret) = (100 ,3736710778780434371)
34 / 38
メタプログラミングのやり方
最終的に評価したい式(出力)と、使える名前や式(入力)を、具体的に書いて並べてみる
入力をどう組み立て・変形すれば出力の式になるのかを考える今回説明した、変数補間や名前保護のルールが身につけば、ある程度のマクロやメタプログラミングは少しの慣れで書けるはず
macroexpand を使って確認するのが良いREPL など、Main モジュールでテストをすると、自動でなされるモジュール名修飾の結果がわかりづらくなるので、できるだけ別のモジュールの中で書いたほうが良い
35 / 38
関数定義
関数定義も AST で表せるので、関数の自動生成なんてこともできる
形がほとんど同じで、部品(使う関数など)の名前だけが違う関数群などでは是非
1 type MyFloat2 val :: Float643 end4 for fn in (:sin , :cos , :tan)5 eval(Expr(:import , :Base , fn))6 @eval ($fn)(mf:: MyFloat) = ($fn)(mf.
val)7 end
36 / 38
関数定義をジャックするマクロ
関数定義を自動的に別の関数定義に置き換えるマクロを作ることもできる
自動ロギング、メモ化、末尾再帰最適化などなど一度に展開形にするのはまず無理
受け取った Expr 型のオブジェクトから関数名や引数名などを抽出する必要があるいっそオブジェクトの追記・書き換えで完成させるのもアリ渡された関数定義を、名前を変えて実行してしまい、新しく作る関数の中から呼ぶのも有効
この場合でも「最後にはこう展開されて欲しい」という対応関係を最初に考えるのが大事
メモ化や末尾再帰最適化など、そもそもどうやって実現するのかを考えるところから始まる基本的には macroexpand で結果を確認したり、dump, @show
で中身を見ながら試行錯誤成果物が役立つかは別としても、かなり勉強・練習になる
サンプルとしてメモ化マクロを作ってみたので興味があればちなみに Memoize.jl なんていうパッケージも既に存在する
37 / 38
まとめ
Julia の構文を Julia の中からいじる方法(メタプログラミング)を見てきた
コードを自動生成することで全体のコード量や実装時間を減らせるマクロや@generated でコンパイル時計算をしたりMemoize.jl などで関数を自動メモ化したりすることで実行性能をあげられる(かもしれない)自分で書く場合、どういう結果が出て欲しいかをまず考えるmacroexpand, dump, @show あたりを駆使して試行錯誤
参考資料Julia 公式 Document
英語が読めるならこれを読みながら手を動かせばよい
On Lisp, (著: Paul Graham, 和訳:野田開)
Lisp 系の言語を学ぶと Julia が多大な影響を受けていることがよくわかるマクロだけじゃなくてクロージャなどの理解にも役立つ
38 / 38
Recommended