Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Metaprogramming in JuliaLang

1,881 views

Published on

Introduction to Metaprogramming of JuliaLang (In Japanese)

JuliaTokyo #3
http://juliatokyo.connpass.com/event/13218/

Published in: Technology
  • Be the first to comment

Metaprogramming in JuliaLang

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

×