どうも、靖宗です。
今回はMacrosということで、超時空要塞ではなくちょいちょいでてたifとかquoteとかの奴です。
Foreword
大いなる力には大いなる責任が伴う、とスパイダーマンがリブートする度に回想されるベンおじさんの言葉があります。
マクロもまた同じで、コードが完結になる力には開発者への責任が伴います。
最終手段として使い、あまりおいそれと使わないほうがいいよ!と書かれています。
Our first macro
マクロの定義はdefmacro/2
で行うそうです。
試しにif
の反対の動作を行うunless
を定義してみましょう。
今回はmacros.exs
というファイルに書き込んで使っていきます。
defmodule Unless do def fun_unless(clause, do: expression) do if(!clause, do: expression) end defmacro macro_unless(clause, do: expression) do quote do if(!unquote(clause), do: unquote(expression)) end end end
基本的にマクロではquoted expression
が渡されます。なのでclause
はunquote
してやって値を代入する必要があります。
expression
に関しても同じです。
実際に使ってみましょう。iex macros.exs
でmacros.exsを読み込んでからiexを起動します。
iex> require Unless iex> Unless.macro_unless true, do: IO.puts "this should never be printed" nil iex> Unless.fun_unless true, do: IO.puts "this should never be printed" "this should never be printed" nil
とりあえずなんか動作はしてるっぽいです。
ただ、関数として定義した方は標準出力に表示が出て、マクロとして定義した方は何も表示されません。
これはdefmacro/2
で定義してるマクロの引数はマクロを実行しただけでは評価されないからだそうです。unless
マクロでは、受け取ったquoted expression
を別のquoted expression
に変換しているだけです。
つまり、
Unless.macro_unless true, do: IO.puts "this should never be printed"
と実行した段階ではmacro_unless
というマクロの定義は
macro_unless(true, [do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["this should never be printed"]}])
というquoted expression
を引数にとり、
{:if, [], [{:!, [], [true]}, [do: {{:., [], [{:__aliases__, [], [:IO]}, :puts]}, [], ["this should never be printed"]}]]}
というquoted expression
を返しているに過ぎません。
このquoted expression
はMacro.expand_once/2
で評価することが可能です。
iex(1)> require Unless Unless iex(2)> expr = quote do: Unless.macro_unless(true, do: IO.puts "this should never be printed") {{:., [], [{:__aliases__, [alias: false], [:Unless]}, :macro_unless]}, [], [ true, [ do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["this should never be printed"]} ] ]} iex(3)> res = Macro.expand_once(expr, __ENV__) # Unlessモジュールのmacro_unlessが評価される {:if, [context: Unless, import: Kernel], [ {:!, [context: Unless, import: Kernel], [true]}, [ do: {{:., [], [ {:__aliases__, [counter: -576460752303423486, alias: false], [:IO]}, :puts ]}, [], ["this should never be printed"]} ] ]} iex(4)> IO.puts Macro.to_string(res) if(!true) do IO.puts("this should never be printed") end :ok
Macro.expand_once/2
は第一引数に評価するquoted expression
を、第二引数に環境(ENV)を指定します。環境に関しては後述します。
だいたいマクロに関してはこんなものだそうです。
Unlessモジュールのmacro_unless
を例に出しましたが、実際にunless/2
はElixir上で
defmacro unless(clause, do: expression) do quote do if(!unquote(clause), do: unquote(expression)) end end
と実装されており、先ほど書いたサンプルと同じである事が分かります。
また、defmacro/2
やdef/2
もマクロで記載されており、マクロを使いこなすと固有の領域で言語の機能を拡張することができます。(→DSL?)
Macros hygiene
quote
内部で定義された変数はマクロが展開された場所の変数とは競合しないようになっているそうです。
文章だけではアレなのでサンプルを見ていきます。
defmodule Hygiene do defmacro no_interference do quote do: a = 1 end end defmodule HygieneTest do def go do require Hygiene a = 13 Hygiene.no_interference a end end HygieneTest.go # => 13
def~~
で囲われたブロックなのでスコープ的にそうなりそうな感じもしますが、今回に限ってはマクロが展開されているのでスコープ云々の話ではありません。
本来、Hygiene.no_interference
の箇所にa=1が展開されてしまうとaの13は上書きされます。
あまり無いとは思うんですが、この変数に影響を及ぼしたい場合(今回であれば代入したい場合)は、var!
を使用します。
defmodule Hygiene do defmacro interference do quote do: var!(a) = 1 end end defmodule HygieneTest do def go do require Hygiene a = 13 Hygiene.interference a end end HygieneTest.go # => 1
一応動作しますがwarning: variable "a" is unused (if the variable is not meant to be used, prefix it with an underscore)
と怒られます。
変数の干渉が起きないのは、変数それぞれにコンテキストが設定されているためです。
前の章で変数は{:x, [], Elixir}
と内部では管理されていることを確認しましたが、この最後の引数のElixir
の箇所でどのモジュールで利用されている変数なのかなどを管理し、2個目の引数で別のコンテキストを管理しているようです。
サンプルでは
{:x, [line: 3], nil}
となっており、行の情報も管理されているみたいなんですが、自分の環境ではでなかったので無視です。
Sampleモジュール内の変数はElixir
ではなくSample
というモジュール名が付与されます。
defmodule Sample do def quoted do quote do: x end end Sample.quoted #=> {:x, [line: 3], Sample}
この情報を付与する機能でimport
やalias
などは実装されているようです。
動的に変数名をvar!
したい場合はMacro.var/2
で実装できるようです。
defmodule Sample do defmacro initialize_to_char_count(variables) do Enum.map variables, fn(name) -> var = Macro.var(name, nil) length = name |> Atom.to_string |> String.length quote do unquote(var) = unquote(length) end end end def run do initialize_to_char_count [:red, :green, :yellow] [red, green, yellow] end end > Sample.run #=> [3, 5, 6]
Macro.var/2
の第二引数に関してはコンテキストが入るそうですが、詳細は次の章で。
The environment
Macro.expand_once/2
を使用したときに__ENV__
を使用しました。
__ENV__
はMacro.Env
のインスタンスだそうで(オブジェクト指向っぽい名称・・・)、コンパイル時の情報など様々な情報が管理されているもののようです。
iex> __ENV__.module nil iex> __ENV__.file "iex" iex> __ENV__.requires [IEx.Helpers, Kernel, Kernel.Typespec] iex> require Integer nil iex> __ENV__.requires [IEx.Helpers, Integer, Kernel, Kernel.Typespec]
Macroモジュールは基本的にこの__ENV__
を利用していろんな機能を実装してるようです。
別ノードの環境データを取得してMacroを生成して云々とかするときはこの__ENV__
がやりとりされるといったところでしょうか。
今のところ直接いじったりするものでもなさそうですし、頭の片隅に置いとく程度にとどめておきます。
Private macros
プライベート関数のようにプライベートマクロをdefmacrop
で実装かのうなようです。
コンパイル時にのみ評価されるマクロを作成したい時なんかはこれを使うんでしょうが、イマイチ使いどころが分かりません。
必要に応じて復習したいと思います。
Write macros responsibly
マクロめっちゃいいよ!って紹介。使って欲しいのか使って欲しくないのかどっちなんだ。
- マクロは衛生的!(変数の汚染が少ない)
- マクロは辞書的!(requireとかimportとかちゃんと書かないと使えない)
- マクロは明示的!(きちんとした手順を踏まないとマクロは使えない)
- マクロの言語は明確!曖昧な表現はない!
とはいえまだまだ自由にしてもいいところがあるのも事実。
協力であるが故に影響力もでかいので、マクロはコンパクトに書くことが推奨されてます。
たとえば
defmodule MyModule do defmacro my_macro(a, b, c) do quote do do_this(unquote(a)) ... do_that(unquote(b)) ... and_that(unquote(c)) end end end
というマクロ。もはや関数。
マクロはコンパクトである事が推奨されているので、下記のように書き換えます。
defmodule MyModule do defmacro my_macro(a, b, c) do quote do # Keep what you need to do here to a minimum # and move everything else to a function MyModule.do_this_that_and_that(unquote(a), unquote(b), unquote(c)) end end def do_this_that_and_that(a, b, c) do do_this(a) ... do_that(b) ... and_that(c) end end
APIとしての機能とマクロとしての機能ははっきりと使い分けようという意思が見て取れます。
マクロに関してはまだまだ慣れない感が拭えませんが、とりあえずガンガン使用するものではないのでライブラリの内部実装とかを見て出てくる度に復習しようと思います。