15
15

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 3 years have passed since last update.

Juliaでメタプログラミング入門

Posted at

この記事は,東京大学航空宇宙工学科/専攻 Advent Calendar 2019 16日目の記事です.
学科の内容とは全く関係ないですが是非最後まで読んでくださると幸いです.
ほとんど公式ドキュメントそのままの内容になのはご了承ください.
初心者なのでかなり間違えてる箇所があるかと思いますので,是非その時はコメントで優しく教えてくださるとうれしいです.
このドキュメントは全てJuliaのREPL環境の中で動かします.
こちらからバイナリをダウンロードして実行するだけで簡単に動かせますので,是非試してみてください.

メタプログラミングとは

まず最初にメタプログラミングとは何かというものについて述べたいと思います.
日本語版wikipediaを参照するとメタプログラミングとは以下のようなものらしいです.

メタプログラミング (metaprogramming) とはプログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。主に対象言語に埋め込まれたマクロ言語によって行われる。

正直よく分かりませんね....
ここで英語版wikipediaを参照すると以下のような記述があります

Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running.In some cases, this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time. It also allows programs greater flexibility to efficiently handle new situations without recompilation.

こちらの方が分かりやすいですね.つまりメタプログラミングとは他のプログラムをデータのように保持や処理が出来るプログラミングのことらしいです.

Juliaにおける表現と抽象構文木

文字列で表現されたコードをMeta.parse関数で Expr 型で表現することが可能になります.

julia> prog = "1 + 1"
"1 + 1"

julia> ex1 = Meta.parse(prog)
:(1 + 1)

julia> dump(ex1)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Int64 1
    3: Int64 1

また Expr()関数を用いることで直接表現することも可能になります.

julia> ex2 = Expr(:call,:+,1,1)
:(1 + 1)

julia> dump(ex2)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Int64 1
    3: Int64 1

またこれは先ほどの Meta.parse関数を用いた場合と等価になります.

julia> ex1 == ex2
true

ここでExprという型に付いて述べたいと思います.
これは抽象構文木(Abstract syntax tree, AST)を表すデータ型になっています.
プログラムコードに対して意味のある情報のみを抽出した木構造になっており,Juliaではこの構造を用いて処理の内容を保持します.
よって木構造なので以下のような表現も可能になります.

julia> ex3 = Meta.parse("(4 + 4) / 2")
:((4 + 4) / 2)

julia> dump(ex3)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol /
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol +
        2: Int64 4
        3: Int64 4
    3: Int64 2

またdump()の出力結果を見ると,Symbolという型が使われているのが分かります.
Symbolはコードの変数を表すのに使われる型になります.Juliaの内部では演算子や変数の区別をこの時点ではしておらず,constなどもSymbolとして用いられます.
またシンボルは名前の先頭に :をつけることで呼び出すことが出来ます.
ただし=だけは:=だとセイウチ演算子と混同する恐れがあるためなのか:(=)で呼び出す必要があります.

julia> ex4 = Expr(:const,Expr(:(=),:x,1))
:(const x = 1)

julia> dump(ex4)
Expr
  head: Symbol const
  args: Array{Any}((1,))
    1: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol x
        2: Int64 1

またExpr()の代わりに:(...)quote...endで処理コードをラップすることでExpr型を得る事が出来ます.
quote...endでラップした場合には行数や定義された場所などが保持されます.

julia> dump(:(const x=1))
Expr
  head: Symbol const
  args: Array{Any}((1,))
    1: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol x
        2: Int64 1

julia> ex5 = quote
       const x=1
       end
quote
    #= REPL[89]:2 =#
    const x = 1
end

julia> dump(ex5)
Expr
  head: Symbol block
  args: Array{Any}((2,))
    1: LineNumberNode
      line: Int64 2
      file: Symbol REPL[89]
    2: Expr
      head: Symbol const
      args: Array{Any}((1,))
        1: Expr
          head: Symbol =
          args: Array{Any}((2,))
            1: Symbol x
            2: Int64 1

最後にExpr型で定義された処理はeval関数を用いることで実行する事ができます

julia> eval(ex1)
2

julia> eval(ex5)
1

julia> @show x
x = 1
1

マクロの作成方法

マクロはJuliaにおいてメタプログラミングととても関わり深いのでよく一緒に紹介されます.マクロでは生成されたコードをプログラムの最終本体に含める事が出来ます.
例えば@timeというマクロを使用すると,処理する時間を測る事が出来ます.
ここではこのようなマクロの作成方法について述べます.

julia> using Random

julia> @time sort(shuffle(1:5))
  0.000019 seconds (7 allocations: 448 bytes)
5-element Array{Int64,1}:
 1
 2
 3
 4
 5

マクロはmacro NAME ... endで処理を囲んで作成します.実行する際には@NAMEと打つだけです.

julia> macro sayhello()
       return :(println("Hello,Wolrd!"))
       end
@sayhello (macro with 1 method)

julia> @sayhello
Hello,Wolrd!

マクロでExpr型で値を返した場合には最後に実行します.
引数を受け取る際には以下のようにします.また多重ディスパッチにも対応してます.

julia> macro sayhello(name)
       return :(println("Hello,",$name))
       end
@sayhello (macro with 2 methods)

julia> @sayhello "Japan!"
Hello,Japan!

julia> macro sayhello(number::Int64)
                     word = :("")
                     for i=1:number
                     word = :($word * "Hello, World!")
                     end
                     return word
                     end
@sayhello (macro with 3 methods)

julia> @sayhello 2
"Hello, World!Hello, World!"

ここで$は補間演算子になります.変数の中身の値を参照するようになります.
@macroexpandを使うとマクロが返すExpr型の中身を見る事ができます.

julia> @macroexpand @sayhello 3
:((("" * "Hello, World!") * "Hello, World!") * "Hello, World!")

メタプログラミングの良いところ

ここでは自分が感じたメタプログラミングの良いところについて述べていきたいと思います.

プログラムそのものにアクセス出来る

先ほどちらっと使いましたが@showと同じ様な事が出来るマクロを自分で簡単に以下のように作ってみてみました.このマクロでは変数名=変数の値を標準出力します.

julia> macro show2(val)
       return :(println($(string(val)),"=",$val))
       end
@show2 (macro with 1 method)

julia> @show2 y
y=4

また違う値が入っていたらAssertionエラーを出す@assertを以下のように作成します. 1==1を入力した際には値が合っているので何も起きませんが,
1==0では値が異なるのでAssrtionエラーと,Assertionエラーになっているコードを表示してます.

julia> macro assert(ex)
       return :( $ex ? nothing : throw(AssertionError($(string(ex)))) )
       end
@assert (macro with 1 method)

julia> @assert 1==1

julia> @assert 1==0
ERROR: AssertionError: 1 == 0

この例のようにメタプログラミングでは変数名などのプログラムに直接アクセスする事が可能なので,プログラムを書く側が分かりやすい出力をする事が可能になります.

実行が早くなる

実行時間を測るのにBenchmarkToolsというものを使用します,以下のコードを実行してインストールしておきましょう.

using Pkg; Pkg.add("BenchmarkTools")

まずgenerated functionに関して説明します. この関数は@generatedというマクロを使って定義する関数で.この関数自体では引数の型にしか参照できず,値にはアクセスする事が出来ません.また返す値はExpr型である必要があります(ただし呼び出された後にこの返り値は実行されます.).以下に例を示します.

julia> @generated function foo(x)
           Core.println(x)
           return :(x * x)
       end
foo (generic function with 1 method)

julia> x = foo(2); # note: output is from println() statement in the body
Int64

julia> x           # now we print x
4

julia> y = foo("bar");
String

julia> y
"barbar"

ここでは与えられた引数の平均値を求める関数を以下のように二つ定義します.

julia> function mean1(objs...)
           total = objs[1]
           for i in 2:length(objs)
               total += objs[i]
           end
           return total/length(objs)
       end
mean1 (generic function with 1 method)

julia> @generated function mean2(objs...)
           total = :(objs[1])
           for i in 2:length(objs)
               total = :($total + objs[$i])
           end
           :($total/$(length(objs)))
       end
mean2 (generic function with 1 method)

型を二種類入れたTupleを用意します.

julia> values = ([i for i=1:0.25:5]...,[i for i=1:10]...)
(1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.25, 4.5, 4.75, 5.0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

julia> typeof(values)
Tuple{Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Float64,Int64,Int64,Int64,Int64,Int64,Int64,Int64,Int64,Int64,Int64}

実際に用意した関数とTupleで実行時間を比較してみましょう

julia>using BenchmarkTools
julia> @btime mean1(values...)
  1.052 μs (44 allocations: 704 bytes)

julia> @btime mean2(values...)
  416.925 ns (18 allocations: 288 bytes)

実行時間を比較すると2.5倍程度,メタプログラミングを使用した関数の方が早くメモリの使用量も少ない結果になりました.
Julia プログラミングクックブックには400倍程度変わるもっと露骨な例が載っています.
平均値を計算する際にmean1では値を直接計算しているのに対してmean2では値を計算してる訳ではなく返しているのもExprというのが主な違いとなっており.その違いによりJuliaのJITコンパイラが最適化する際にgenerated functionの方が最適化しやすいという訳らしいです.

最後に

簡単に短い内容ではありましたがJuliaでのメタプログラミングに関して興味を持っていただけましたでしょうか.
今後もちょっとずつ勉強していくつもりではあるのでまた何か書けそうなネタがあれば書いていきたいなと思っております.

参考

Julia プログラミングクックブック
Juliaのシンボルとは?
Metaprogramming · The Julia Language

15
15
0

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
15
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?