3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Julia MacroTools

Posted at

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 なども有用そう。

3
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?