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

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


# メタプログラミングとは

まず最初にメタプログラミングとは何かというものについて述べたいと思います．
[日本語版wikipedia](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%BF%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)を参照するとメタプログラミングとは以下のようなものらしいです．
> メタプログラミング (metaprogramming) とはプログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。主に対象言語に埋め込まれたマクロ言語によって行われる。

正直よく分かりませんね....
ここで[英語版wikipedia](https://en.wikipedia.org/wiki/Metaprogramming)を参照すると以下のような記述があります
> 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
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
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
julia> ex1 == ex2
true
```

ここで`Expr`という型に付いて述べたいと思います．
これは[抽象構文木(Abstract syntax tree, AST)](https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8)を表すデータ型になっています．
プログラムコードに対して意味のある情報のみを抽出した木構造になっており，Juliaではこの構造を用いて処理の内容を保持します.
よって木構造なので以下のような表現も可能になります．


```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
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
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
julia> eval(ex1)
2

julia> eval(ex5)
1

julia> @show x
x = 1
1
```

# マクロの作成方法

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

```julia
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
julia> macro sayhello()
       return :(println("Hello,Wolrd!"))
       end
@sayhello (macro with 1 method)

julia> @sayhello
Hello,Wolrd!
```
マクロで`Expr`型で値を返した場合には最後に実行します．
引数を受け取る際には以下のようにします．また[多重ディスパッチ](https://ja.wikipedia.org/wiki/%E5%A4%9A%E9%87%8D%E3%83%87%E3%82%A3%E3%82%B9%E3%83%91%E3%83%83%E3%83%81)にも対応してます．

```julia
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
julia> @macroexpand @sayhello 3
:((("" * "Hello, World!") * "Hello, World!") * "Hello, World!")
```

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

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

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

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

```julia
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
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というものを使用します，以下のコードを実行してインストールしておきましょう．

```julia
using Pkg; Pkg.add("BenchmarkTools")
```

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

```julia
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
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
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
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 プログラミングクックブック](https://www.amazon.co.jp/Julia%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%AF%E3%83%83%E3%82%AF%E3%83%96%E3%83%83%E3%82%AF-%E2%80%95%E8%A8%80%E8%AA%9E%E4%BB%95%E6%A7%98%E3%81%8B%E3%82%89%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E6%9E%90%E3%80%81%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%80%81%E6%95%B0%E5%80%A4%E8%A8%88%E7%AE%97%E3%81%BE%E3%81%A7-Bogumi%C5%82-Kami%C5%84ski/dp/4873118891/ref=asc_df_4873118891/?tag=jpgo-22&linkCode=df0&hvadid=342518373760&hvpos=1o2&hvnetw=g&hvrand=13470067795715434505&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009333&hvtargid=pla-887762074047&psc=1&th=1&psc=1)には400倍程度変わるもっと露骨な例が載っています.
平均値を計算する際に`mean1`では値を直接計算しているのに対して`mean2`では値を計算してる訳ではなく返しているのもExprというのが主な違いとなっており．その違いによりJuliaのJITコンパイラが最適化する際にgenerated functionの方が最適化しやすいという訳らしいです．

# 最後に

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

# 参考

[Julia プログラミングクックブック](https://www.amazon.co.jp/Julia%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%AF%E3%83%83%E3%82%AF%E3%83%96%E3%83%83%E3%82%AF-%E2%80%95%E8%A8%80%E8%AA%9E%E4%BB%95%E6%A7%98%E3%81%8B%E3%82%89%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E6%9E%90%E3%80%81%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%80%81%E6%95%B0%E5%80%A4%E8%A8%88%E7%AE%97%E3%81%BE%E3%81%A7-Bogumi%C5%82-Kami%C5%84ski/dp/4873118891/ref=asc_df_4873118891/?tag=jpgo-22&linkCode=df0&hvadid=342518373760&hvpos=1o2&hvnetw=g&hvrand=13470067795715434505&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009333&hvtargid=pla-887762074047&psc=1&th=1&psc=1)
[Juliaのシンボルとは?](http://bicycle1885.hatenablog.com/entry/2015/08/15/143620)
[Metaprogramming · The Julia Language](https://docs.julialang.org/en/v1/manual/metaprogramming/)