Elixirの各種パッケージ内に、時々下記のようなコードが登場します。
defmodule Module1 do
〜省略〜
def hoge(unquote(p)) do
〜省略〜
end
end
unquoteに関してはこちらやこちらで説明されていますが、関数の引数をunquoteする構文に関しては説明されていません。
ということでこの用法に関して調査してみました。
サンプル1
本題に入る前に、まず下記のコードを見て頂ければと思います。
defmodule Module1 do
a = "hoge"
b = "moge"
end
このモジュールのa = "hoge"
やb = "moge"
というコードは、いつ実行されるのでしょうか?
答えは「コンパイル時」です。コンパイル後には上記のコードは消えてしまいます。
逆に言うと、「モジュール内ではコンパイル時にコードを実行できる」ということになります。
サンプル2
次に、下記のコードを見て頂ければと思います。
defmodule Module1 do
p = "hoge"
def func1 do
unquote(p)
end
end
Module1.func1
# => "hoge"
上記のコードは問題なく実行できます。unquote
はquote
の内部でしか使えないというのがルールですが、defmodule
やdef
も実際にはマクロなので、上記のfunc1のdoブロックはdef
マクロ内のquote式内で展開されるため、この構文はエラーになりません。
イメージとしては下記みたいなものだと考えて良いのではないかと思います。
defmodule Module1 do
p = "hoge"
quote do
unquote(p)
end
end
ご興味のある方はElixirのKernelモジュール内でdefが定義されている箇所をご参照頂ければと思います。
サンプル3
ここからが本題です。下記のコードを見て頂ければと思います。
defmodule Module1 do
p = "hoge"
def func1(unquote(p)) do
"func1 called"
end
end
Module1.func1("foo")
# => ** (FunctionClauseError) no function clause matching in Module1.func1/1
コンパイルは正常に完了し、Module1.fun1/1
という関数も確かに定義されていますが、Module1.func1("foo")
のように実行してみると、FunctionClauseError
が発生します。
つまり、Module1.fun1/1
という関数は存在するけど、引数がマッチしなかったよというエラーが発生しているということです。下記の呼び出しも全てFunctionClauseError
になります。
Module1.func1(100)
Module1.func1({100})
Module1.func1([a: 100])
Module1.func1(%{a: 100})
どのような引数を渡すとこの関数を実行することが出来るのでしょうか?
答えは、下記のようになります。
Module1.func1("hoge")
要するに、この関数の引数としてパターンマッチが成功するのは"hoge"だけだということになります。
つまり、Module1.func1/1
という関数は、下記のように定義されているということです。
defmodule Module1 do
def func1("hoge") do
"func1 called"
end
end
引数として、変数ではなく文字列がそのまま指定されていますので、この文字列とパターンマッチする引数を渡さないと、すべてFunctionClauseError
になる、ということになります。
つまり、関数の引数にunquote
を指定する構文は、要するにパターンマッチなわけですが、その中でも最も制限が厳しい用法、
実行時に渡される引数の「型」ではなく「値」を制限する
という目的で使用されている、ということになります。
考察
上記のような構文は、PhoenixやEctoの内部で散見されますが、用途としてはおそらく「引数チェックの省略」「不具合の早期発見」「安全性の確保」等のためだと思われます。
マクロ内で定義された関数に渡される引数が、「型」や「キー名」等だけでなく、「値」も含めてコンパイル時の定義と完全に一致することが必要になり、一致しない場合はFunctionClauseError
をraiseしてくれますので、関数内での引数チェックが不要になりますし、不具合を早期に発見することが可能になる、そういう目的で使用されているのではないかと思われます。
注意
上記のサンプルコードでは、引数をunquote(p)
のように直接unquote
に渡していましたが、この場合pは「クォート式に変換されている」必要があります。
上記のサンプルでは、pはクォート式に変換されていませんが、これはpに設定している値が文字列
のため、クォート式に変換しても内部表現が変わらないのでこの方式でオーケーというだけで、タプルやマップや構造体を使用する場合はクォート式に変換してから渡す必要があります。
具体的には、下記のようなコードは、
defmodule Module1 do
p = %{name: "hoge"}
def func1(unquote(p)) do
"func1 called"
end
end
# => ** (CompileError) iex: invalid quoted expression: %{name: "hoge"}
上記のようにCompileError
が発生します。正常にコンパイルするには、
defmodule Module1 do
p = Macro.escape(%{name: "hoge"}) # エスケープする
def func1(unquote(p)) do
"func1 called"
end
end
Module1.func1(%{name: "hoge"})
# => "func1 called"
というように記述する必要があります。
ご参考までに、各型がクォート式に変換された場合の出力結果を下記に記述しておきます。
quote do: 1
# => 1
quote do: "2"
# => "2"
quote do: [c: 3]
# => [c: 3]
quote do: {:d, 4, 4}
# => {:{}, [], [:d, 4, 4]}
quote do: %{e: 5}
# => {:%{}, [], [e: 5]}
defmodule F do defstruct name: "name" end
quote do: %F{}
# => {:%, [], [{:__aliases__, [alias: false], [:F]}, {:%{}, [], []}]}
また、func1
に渡された引数p
を参照したい場合、下記のようなコードはCompileError
になります。
defmodule Module1 do
p = Macro.escape(%{name: "hoge"})
def func1(unquote(p)) do
p
end
end
# => ** (CompileError) iex:1033: undefined function p/0
関数内で変数を参照する場合、その変数は当然そのスコープ内に存在しなければなりません。
上記の場合、func1
の引数pは展開されて%Module0{name: "kenta"}
という状態になっていますので、このスコープ内にpという変数はまだ存在していません。そのためコンパイルエラーになるわけです。
正常にコンパイルするには、下記のように記述する必要があります。
defmodule Module1 do
p = Macro.escape(%{name: "hoge"})
def func1(unquote(p)) do
unquote(p) # unquoteする
end
end
Module1.func1(%{name: "hoge"})
# => %{name: "hoge"}
ちなみに、unquote
しなくても、func1
内でpを「右辺」ではなく「左辺」に使った場合、エラーにはなりません。
defmodule Module1 do
p = Macro.escape(%{name: "hoge"})
def func1(unquote(p)) do
p = "foo" # エラーにならない(pという新しいローカル変数が定義される)
end
end
Module1.func1(%{name: "hoge"})
# => "foo"
この場合、単純にp
というローカル変数がfunc1
内に定義されるだけなので、コンパイルが正常に通る、ということになります。
上記と、以前解説させて頂いたvar!
マクロを組み合わせると、下記のようなマクロが作成出来ます。
defmodule Module1 do
defmacro macro1 p1 do
quote do
def func1(unquote(p1)) do
var!(p1) = unquote(p1)
IO.inspect "var!(p1) = #{inspect var!(p1)}"
Module1.macro2
end
end
end
defmacro macro2 do
quote do
var!(p1) = "foo"
IO.inspect "var!(p1) = #{inspect var!(p1)}"
end
end
end
defmodule Module2 do
import Module1
macro1 "hoge"
end
Module2.func1("hoge")
# "var!(p1) = \"hoge\""
# "var!(p1) = \"foo\""
この、unquote
した引数を、var!
マクロを適用したnilコンテキスト変数に割り当てるという方法は、Phoenixのコードの一部で見受けられる方法ですが、この用法の目的は、
引数の値を制限した上で、ネストされるマクロからその値を参照出来るようにする
ということになるようです。
最後に
Elixirの内部的な挙動にまだあまり詳しくないので、かなり予測を含んだ考察になります。誤り等ありましたら是非ご指摘くださいm(__)m
(2016/03/16追記)
Metaprogramming Elixirによると、マクロ内で関数を定義する際に、関数名や関数の引数にunquote
を使う手法はunquote fragments
と呼ばれているそうです。