Hygienicなにそれ美味しいの
マクロ内部と呼び出し側で共通の変数を使うためのメモ。
本来Elixirのマクロは"Hygienic"(衛生的)で、マクロ側で作った変数は呼び出し側の変数に影響を与えない。逆も然り。同じ名前の変数を作っても他方が上書きされることはない、汚染することはない、という意味で衛生的。
defmacro my_macro do
quote do
a = 1
end
end
a = 2
my_macro()
IO.puts a #=> 2
以下、衛生的でないことを書く。
読み
そのまま変数与えて読む場合。めっちゃ普通。
defmacro read(var) do
quote do
v = unquote(var)
IO.inspect v
end
end
foo = 1
read(foo) #=> 1
アトム与えて、対応する変数の中身を読む場合。Macro.var/2
使う。
defmacro read(name) when is_atom(name) do
quote do
v = unquote(Macro.var name, nil)
IO.inspect v
end
end
foo = 2
read(:foo) #=> 2
デフォルトではhoge
を読むけど、明示されたらそっち読む場合。
defmacro read(var \\ Macro.var(:hoge, nil)) do
quote do
v = unquote(var)
IO.inspect v
end
end
hoge = 3
fuga = 4
read() #=> 3
read(fuga) #=> 4
書き
引数に変数与えて、中身が書き換わって返ってくるやつ。
defmacro bind(var, value) do
quote do
unquote(var) = unquote(value)
end
end
bind(foo, 5)
foo #=> 5
アトム与えて、対応する変数をつくりたい場合。既存の変数名を与えると上書きされる。
defmacro bind(name, value) when is_atom(name) do
quote do
unquote(Macro.var name, nil) = unquote(value)
end
end
bind(:bar, 6)
bar #=> 6
是非
ドキュメントに書いてあろうとも、勝手に変数の中身を上書きするのは決してお行儀が良いとは言えないので、書き換えは行うべきでないと思う。特定の名前の変数があることを前提にそれを読むライブラリは存在していて、例えばplug_test_helpers。Plugのコネクションは変数conn
に入れるのが慣習になっているので、それを規約としている。
# plug_test_helpersのREADMEから引用
test "headers" do
conn = conn(:get, "/image.jpg")
conn = MyPlug.call(conn, @opts)
assert_header "content_type"
assert_header "content_type", "image/jpg"
assert_header_match "content_type", ~r/\Aimage\/jpe?g\Z/
end
conn
は用意しているけれど、アサーションマクロには渡していないのがわかる。マクロは勝手にconn
を読んでテストする。これは良く言えばCoCなのだけれど、渡していない変数が使われているということには若干の気持ち悪さがある。個人的には、アサーションは第一引数にconn
を取り、戻り値としてconn
を返す、というのが良いと思った。そうすれば、パイプで以下のように書ける。
test "headers" do
conn(:get, "/image.jpg")
|> MyPlug.call(@opts)
|> assert_header("content_type")
|> assert_header("content_type", "image/jpg")
|> assert_header_match("content_type", ~r/\Aimage\/jpe?g\Z/)
end
やっぱり渡した変数を使ってくれたほうが読みやすいと思う。もちろん読みやすさ以上に優先すべき利点があると判断されるなら、特定の変数の存在を規約とするのも悪ではない。
なんか結局、そのときどきに考えろみたいになってしまった。