Julia のマクロとパターンマッチ
Julia には強力なマクロ機能があるが、引数のASTを分解するのに便利なパターンマッチ機構が、言語でサポートされていないので、やや使いにくい。
MLStyleを入れるとパターンマッチが使えるようになるが、構造体の分解はサポートされていない? ようで、ASTの分解にはあまり役に立たない。
MacroTools
どうするのが正しいのか調べてみたらMaroToolsというものがあった。
これは、その名の通りマクロの定義に便利な関数などをまとめたもの。
capture
capture
はパターンマッチを行い、パターン中に書いた変数に部分式をマッチしてくれる。
e = :(
function test(a, b)
a + b
end
)
このような関数定義式は、次のようにパターンマッチできる。
@capture(e, function fname_(args__) body_ end)
fname_
のようにアンダースコアがついた変数がプレースホルダになり、
アンダースコア無しの変数にマッチした部分式を束縛してくれる。
また、args__
のようにアンダースコアが複数ついた変数は、複数の部分式にマッチし、それらをベクタに入れて、アンダースコアをとった名前の変数に束縛する。
julia> fname
:test
julia> args
2-element Vector{Any}:
:a
:b
julia> body
quote
a + b
end
また、@capture マクロ自体が真偽値を返し、失敗時にはfalseとなるので、複数のパターンのいずれかにマッチさせるような書き方も容易だ。
型指定
プレイスホルダにマッチするAST要素の型を指定することができる。アンダースコアの後ろに型を書く。
# マッチする
julia> @capture(:(foo("a")), foo(x_String))
true
# マッチしない
julia> @capture(:(foo("a")), foo(x_Symbol))
false
複数の型のいずれかにマッチさせることもできる。
julia> @capture(:(foo("a")), foo(x_String_Symbol))
true
julia> @capture(:(foo(a)), foo(x_String_Symbol))
true
文字列の場合、interpolation がある文字列はASTでの表現が違うので注意が必要。
julia> dump(:("aa$(a)bb"))
Expr
head: Symbol string
args: Array{Any}((3,))
1: String "aa"
2: Symbol a
3: String "bb"
julia> dump(:("aa1bb"))
String "aa1bb"
interpolationのある文字列には全部小文字のstring
でマッチするので、interpolationのあるなしに関わらずマッチさせるには x_String_string
のように書く必要がある。
Union マッチ
複数のパターンのいずれかにマッチさせることもできる。|
でパターンを区切る。
Juliaには関数定義の際に、function
キーワードを使用する方法としないほうほうがあるが、いずれにもマッチするようにするには以下のように書く。
@capture(ex, (f_(args__) = body_) | (function f_(args__) body_ end))
いずれかのパターンで使用されないプレースホルダに対応する変数にはnothing
が束縛される。
関数定義のパースと合成
Juliaの関数定義は大変複雑なので、パース関数splitdef
が提供されている。
e2 = :(
function add(a::X, b::X) where X <: Real
a+b
end
)
のようにASTを定義しておいて、この関数を適用すると、パース結果の格納されたディクショナリが返される。
julia> splitdef(e2)
Dict{Symbol, Any} with 5 entries:
:name => :add
:args => Any[:(a::X), :(b::X)]
:kwargs => Any[]
:body => quote…
:whereparams => (:(X <: Real),)
逆に、このディクショナリを用意して与えると、ASTを作ってくれる関数combinedef
も用意されている。
combinedef(splitdef(e))
とするとe
が再現される。
これらを使うと、たとえば関数名を変更する(後ろに_2
をつける)ような関数を定義するマクロが次のように書ける。
macro add_name_2(fdef)
d = splitdef(fdef)
d[:name] = Symbol(string(d[:name]) * "_2")
combinedef(d)
end
これを使って次のように定義すると、testA
ではなく testA_2
が定義される。
@add_name_2 function testA(x, y)
x + y
end
splitdef がいろいろな関数定義方法に対応しているので、短い書き方でも動作する。便利。
@add_name_2 testB(x, y) = x + y
splitarg, combinearg
splitarg
は関数定義の引数定義の解析を行い、
(arg_name, arg_type, is_splat, default)
の形のタプルを返す。
julia> e = :( test(x::Integer=0) = x )
julia> splitarg(e.args[1].args[2])
(:x, :Integer, false, 0)
これの逆操作を行うcombinearg
も用意されている。
Expression Walking
式をトラバースして、各ノードに対して指定された関数を適用する、prewalk
, postwalk
が用意されている。
その他
他にもいろいろ用意されているようだ。
行番号情報を削る rmlines
なども有用そう。