38
まっくろわーるど じゅりあらんぐッ! yomichi JuliaTokyo #3 2015/04/25 スライドとサンプルコード https://github.com/yomichi/JuliaTokyo3 1 / 38

Metaprogramming in JuliaLang

Embed Size (px)

Citation preview

Page 1: Metaprogramming in JuliaLang

まっくろわーるどじゅりあらんぐッ!

yomichi

JuliaTokyo #3 2015/04/25

スライドとサンプルコードhttps://github.com/yomichi/JuliaTokyo3

1 / 38

Page 2: Metaprogramming in JuliaLang

自己紹介

HN : 夜道, yomichi

twitter : @yomichi_137

ぽすどくにねんせい統計物理学・計算物理学

主にモンテカルロ法とか言語は C++, Python, Julia

イベント(勉強会)実況勢最近だと全ゲ連(同人ゲーム開発)とか、PyConJP (Python) とか、JuliaTokyoとか

Julia nightly build 勢

Julia の開発環境は REPL と Vim

コミケで Julia 本出したりしてますblog とか締め切りがないので書けません><抽選受かっていたら次の夏にも出します

多分今回の話をもう少し詳しく書きます

(thanks @am_11)

2 / 38

Page 3: Metaprogramming in JuliaLang

今日のお話

1 Symbol 型と Expr 型 – Julia の抽象構文木2 マクロシステム3 メタプログラミング – どう使うか

メタプログラミングとかマクロ展開とかのお話です(Julia の中では)難しそう or 難しいためか余り触れられてこない話確実に 30分では終わらないので適当に飛ばします

一応スライドだけでも読めるつもりで作った 1 ので、興味ある方はあとでゆっくりとどうぞいつものことですが公式ドキュメントが一番詳しくてわかりやすく、ほぼ常に最新なので、英語が苦じゃない人はそちらがおすすめ

未確認で進行形ネタを仕込もうかと思ったけれどそんな余裕がありませんでした

なのでタイトルは出オチです1だから詰め込みすぎになった、とも言う

3 / 38

Page 4: Metaprogramming in JuliaLang

Outline

1 Symbol 型と Expr 型 – Julia の抽象構文木

2 マクロシステム

3 メタプログラミング – どう使うか

4 / 38

Page 5: Metaprogramming in JuliaLang

Julia の抽象構文木 (AST)

例えば x + (y + z) という式は Juliaの中の人からは右図のように木構造(抽象構文木)として見えている

:call は関数呼び出しの意味+ は中置が認められているというだけで普通の関数であることに注意つまり正確には +(x, +(y, z))

Julia は式をこのように抽象構文木に変換して、それから式の評価をしている

この構造をそのまま保持することで、式やプログラムそのものをデータとして扱える(=同図像性)プログラムを生成するプログラムも書ける(=メタプログラミング)

:call

:call:+

:+

:x

:y :z

5 / 38

Page 6: Metaprogramming in JuliaLang

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

Page 7: Metaprogramming in JuliaLang

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

Page 8: Metaprogramming in JuliaLang

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

Page 9: Metaprogramming in JuliaLang

変数補間 (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

Page 10: Metaprogramming in JuliaLang

変数補間で 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

Page 11: Metaprogramming in JuliaLang

式の評価

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

Page 12: Metaprogramming in JuliaLang

式の操作

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

Page 13: Metaprogramming in JuliaLang

第一級オブジェクトとしての 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

Page 14: Metaprogramming in JuliaLang

Outline

1 Symbol 型と Expr 型 – Julia の抽象構文木

2 マクロシステム

3 メタプログラミング – どう使うか

14 / 38

Page 15: Metaprogramming in JuliaLang

マクロ

マクロは macro キーワードで定義して、@name という形で呼び出すマクロがやること

1 引数をそれぞれ:() で quote して2 普通の関数と同様に何か仕事して3 返ってきた値を eval する

AST を受け取って AST を返す関数の場合、いちいち quoteや eval をする必要があった

eval( foo( :( x+1 ) ) )

マクロでは @foo(x+1) と書ける@foo x+1 のようにも書ける

マクロは関数と違って健全であるという特徴もある説明は次頁

15 / 38

Page 16: Metaprogramming in JuliaLang

変数捕捉と健全な (hygienic)マクロ

マクロは ASTを変換(マクロ展開)して、新しい ASTを呼び出し元に貼り付ける展開された ASTに含まれる名前が、呼び出し元の文脈にあるものと衝突する事がある(=変数捕捉)

1 必要な名前が呼び出し元で別の値に束縛されている事がある2 呼び出し元の変数を再束縛してしまう

名前の付け方に細工をすることで回避する1 マクロが定義されているモジュール名を使って名前を修飾する2 変数に値を代入するときは、重複しない(今まで作られていなくて、これからも作られない)名前を生成して変数名とする

Base.gensym で生成可能

Julia のマクロ展開では全て自動でやってくれる(健全なマクロ)

16 / 38

Page 17: Metaprogramming in JuliaLang

変数捕捉と健全なマクロ

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

Page 18: Metaprogramming in JuliaLang

変数捕捉と健全なマクロ

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

Page 19: Metaprogramming in JuliaLang

意図的な変数捕捉

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

Page 20: Metaprogramming in JuliaLang

意図的な変数捕捉

このマクロを使うと 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

Page 21: Metaprogramming in JuliaLang

潔癖症レベルで健全

マクロ引数に含まれている変数名も全て保護されるつまり呼び出し元とは関係ないものとなるまず確実に 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

Page 22: Metaprogramming in JuliaLang

回避例

基本的にやることはさっきと同じ今回の@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

Page 23: Metaprogramming in JuliaLang

近未来

この「健全すぎる」という件は 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

Page 24: Metaprogramming in JuliaLang

近未来

もちろん直に書いた名前は今までどおり保護されることとなる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

Page 25: Metaprogramming in JuliaLang

近未来

従来の 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

Page 26: Metaprogramming in JuliaLang

Outline

1 Symbol 型と Expr 型 – Julia の抽象構文木

2 マクロシステム

3 メタプログラミング – どう使うか

26 / 38

Page 27: Metaprogramming in JuliaLang

マクロの効用 – 評価タイミングの制御

マクロ呼出しでは式を式のまま、値に評価せずに渡すので、評価タイミングを自分で制御できる評価しなかったり、複数回評価したりも可能

与えた式の実行にかかる時間を計測する@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

Page 28: Metaprogramming in JuliaLang

マクロの効用 – コンパイル時計算

マクロ展開は式のパース及び関数の 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

Page 29: Metaprogramming in JuliaLang

マクロの効用 – コンパイル時計算

マクロを使わないと毎回正規表現を作ることになる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

Page 30: Metaprogramming in JuliaLang

マクロの効用 – コンパイル時計算

非マクロ版

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

Page 31: Metaprogramming in JuliaLang

Generated function

4/21 に新しく Base.@generated が導入されたドキュメントも同時に追加された

これまでに渡されたことのない型の組み合わせの時は、関数本体を実行する

その時、引数の値は参照できず、自動的に型が得られる同じ型の組み合わせを再度投げると、本体は実行されず返り値のみが得られる

AST を返すようにすることで、マクロのように使うことができるマクロと違い型による多重ディスパッチが働くことが利点関数本体の処理内容によっては、2回目以降も実行されることがあるらしい

引数の型別にコンパイル時計算が可能パラメタライズ型の型パラメータに整数などを渡せて、違う整数では違う型になることを利用すると、数値ごとにコンパイル時計算しておくことができる

31 / 38

Page 32: Metaprogramming in JuliaLang

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

Page 33: Metaprogramming in JuliaLang

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

Page 34: Metaprogramming in JuliaLang

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

Page 35: Metaprogramming in JuliaLang

メタプログラミングのやり方

最終的に評価したい式(出力)と、使える名前や式(入力)を、具体的に書いて並べてみる

入力をどう組み立て・変形すれば出力の式になるのかを考える今回説明した、変数補間や名前保護のルールが身につけば、ある程度のマクロやメタプログラミングは少しの慣れで書けるはず

macroexpand を使って確認するのが良いREPL など、Main モジュールでテストをすると、自動でなされるモジュール名修飾の結果がわかりづらくなるので、できるだけ別のモジュールの中で書いたほうが良い

35 / 38

Page 36: Metaprogramming in JuliaLang

関数定義

関数定義も 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

Page 37: Metaprogramming in JuliaLang

関数定義をジャックするマクロ

関数定義を自動的に別の関数定義に置き換えるマクロを作ることもできる

自動ロギング、メモ化、末尾再帰最適化などなど一度に展開形にするのはまず無理

受け取った Expr 型のオブジェクトから関数名や引数名などを抽出する必要があるいっそオブジェクトの追記・書き換えで完成させるのもアリ渡された関数定義を、名前を変えて実行してしまい、新しく作る関数の中から呼ぶのも有効

この場合でも「最後にはこう展開されて欲しい」という対応関係を最初に考えるのが大事

メモ化や末尾再帰最適化など、そもそもどうやって実現するのかを考えるところから始まる基本的には macroexpand で結果を確認したり、dump, @show

で中身を見ながら試行錯誤成果物が役立つかは別としても、かなり勉強・練習になる

サンプルとしてメモ化マクロを作ってみたので興味があればちなみに Memoize.jl なんていうパッケージも既に存在する

37 / 38

Page 38: Metaprogramming in JuliaLang

まとめ

Julia の構文を Julia の中からいじる方法(メタプログラミング)を見てきた

コードを自動生成することで全体のコード量や実装時間を減らせるマクロや@generated でコンパイル時計算をしたりMemoize.jl などで関数を自動メモ化したりすることで実行性能をあげられる(かもしれない)自分で書く場合、どういう結果が出て欲しいかをまず考えるmacroexpand, dump, @show あたりを駆使して試行錯誤

参考資料Julia 公式 Document

英語が読めるならこれを読みながら手を動かせばよい

On Lisp, (著: Paul Graham, 和訳:野田開)

Lisp 系の言語を学ぶと Julia が多大な影響を受けていることがよくわかるマクロだけじゃなくてクロージャなどの理解にも役立つ

38 / 38