Elixir
ElixirDay 4

マクロと変数

More than 3 years have passed since last update.

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

やっぱり渡した変数を使ってくれたほうが読みやすいと思う。もちろん読みやすさ以上に優先すべき利点があると判断されるなら、特定の変数の存在を規約とするのも悪ではない。

なんか結局、そのときどきに考えろみたいになってしまった。