ElixirのASTって、Lipsの文法をTupleで表現しているようなものなのですね。defmacro、quoteをMacro.expand_onceなんかで分解してみると、 {atom, [List], [List]}の形式で分解されていき、Lipsのような読み方で読み解くことができる、と。
なので、Elixirは
- Elixirの文法そのままでHigh Levelな表現で処理を記述する
- LipsのようなLow Levelな表現で処理系を記述する
という2層で処理を記述することができる、と。High/Lowは、処理の抽象度を指しています。
後者がMacroになるわけですが、そんなMacroも下手に適用範囲を広げないためにcontextによる適用範囲を限定する機能を持っています。
Elixir Hygiene
以下の通り、 __MODULE__ により、contextが異なることがわかります。
quote で囲まれるところは、つまるところ、呼び出された側のcontextに限定された環境下でのみ、有効になる、ということを意味する。
Elixirはこのようにcontextを設定することで、macroの影響範囲を限定する手段をとっています。
defmodule Macro do
defmacro definfo do
IO.puts "In macro's context: (#{__MODULE__})"
quote do
IO.puts "In caller's context: (#{__MODULE__})"
def info do
IO.puts """
My name is #{__MODULE__}
My functions are #{inspect __info__(:functions)}
"""
end
end
end
end
defmodule MyModule do
require Mod
Macr.definfo
end
# In macro's context: (Elixir. Macr)
# In caller's context: (Elixir.MyModule)
# iex(1)> MyModule.info
# My name is Elixir.MyModule
# My functions are [info: 0]
#
# :ok
Violate hygiene!!
通常、 quote で囲まれた範囲は、quoteで囲まれた中だけで有効です。
それ以外の領域には影響を及ぼすことはありません。
例えば、以下の通り name の変数は Setter.bind_name のマクロの範囲を出ることはありません。
defmodule Setter do
defmacro bind_name(string) do
quote do
name = unquote(string)
end
end
end
# iex(1)> require Setter
# nil
# iex(2)> name = "Neko"
# "Neko"
# iex(3)> Setter.bind_name "Inu"
# "Inu"
# iex(4)> name
# "Neko"
しかし、以下のように var!/1 を使うとこのhygieneな領域を超えて変数をOverrideすることが可能になります。この機能は、本当に必要なときのみ使いましょう、と強く書かれてもいます。
defmodule Setter do
defmacro bind_name(string) do
quote do
var!(name) = unquote(string)
end
end
end
# iex(1)> require Setter
# nil
# iex(2)> name = "Neko"
# "Neko"
# iex(3)> Setter.bind_name "Inu"
# "Inu"
# iex(4)> name
# "Inu"
他メモ
Logger.debugは、productionのコンパイルが行われるときに削除される。
なるほど…
Macroは処理系を書き換えてしまって、例えば Kernel.if でさえ異なる処理に書き換えることもできるので少し慎重になっていましたが、影響範囲を限定的にできる、その領域を超えるときは明示が必要、という姿勢が良いですね。
ElixirのMacroのドキュメントに以下が書かれていることからも、やっぱり明示に寄せている方に寄せておきたいですね。
Remember that explicit is better than implicit. Clear code is better than concise code.
追記 20160321
Elixir1.2.3ですが、 var と var! のコードを添付。これを見ると、 var! ではASTを直接操作していることがわかります。なるほど。
1 Comment