Elixir の import/2
を同じモジュールにたいして複数回実行すると、最後に実行したものが優先される。
iex(1)> import List
List
iex(2)> first([1])
1
iex(3)> import List, only: [flatten: 1]
List
iex(4)> first([1])
** (CompileError) iex:4: undefined function first/1
上記の例では、iex(1)
で import List
したときには参照できていた List.first/1
が、iex(4)
の import List, only: [flatten: 1]
のあとでは参照できなくなっている。
この挙動を知らずに失敗した話
Phoenix のコントローラーで「認証済みのユーザーが conn にあれば処理を続行、そうでなければエラー」という処理が頻発するので、次のようなモジュールを書いた。
defmodule AuthUserAction do
defmacro __using__(_opts) do
quote do
import Plug.Conn
import Phoenix.Controller, only: [json: 2, action_name: 1]
def action(conn, _) do
with {:ok, user_id} <- Map.fetch(conn.private, :user_id),
%User{} = user <- Repo.get(User, user_id) do
apply(_MODULE__, action_name(conn), [conn, conn.params, user])
else
_ ->
conn
|> put_status(:forbidden)
|> json(%{
error: "forbidden"
})
end
end
end
end
end
これを使うコントローラーでは、毎回、
use Phoenix.Controller
def my_action(conn, params) do
with {:ok, user_id} <- Map.fetch(conn.private, :user_id),
%User{} = user <- Repo.get(User, user_id) do
...
end
end
のように書いていた処理を、use AuthUserAction
を追加するだけで、
use Phoenix.Controller
use AuthUserAction
def my_action(conn, params, user) do
...
end
このように簡略化できる...はずだった。
コンパイル・エラー
しかし、実装してコンパイルしてみると、以下のようなエラーが出るようになってしまった。
== Compilation error in file lib/my_app/controllers/my_controller.ex ==
** (CompileError) lib/my_app/controllers/my_controller.ex:1: undefined function put_new_layout/2
(elixir 1.10.1) src/elixir_locals.erl:114: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
(stdlib 3.11.2) erl_eval.erl:680: :erl_eval.do_apply/6
(elixir 1.10.1) lib/kernel/parallel_compiler.ex:233: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
何故こうなるか、というと、最初に説明した通り、マクロ中の
import Phoenix.Controller, only: [json: 2, action_name: 1]
が、コントローラー側の use Phoenix.Controller
で import
されてる put_new_layout/2
を上書きしてしまうからだ。
解決
Elixir の import/2
はレキシカル・スコープなので、import を関数内に移動するだけで解決できる。
defmodule AuthUserAction do
defmacro __using__(_opts) do
quote do
def action(conn, _) do
import Plug.Conn
import Phoenix.Controller, only: [json: 2, action_name: 1]
...
end
end
end
end
実際には、コードサイズを減らすためにも、action/2 の実装をマクロの外に出した。
defmodule AuthUserAction do
import Plug.Conn
import Phoenix.Controller, only: [json: 2, action_name: 1]
defmacro __using__(_opts) do
quote do
def action(conn, _) do
AuthUserAction.action(conn, __MODULE__)
end
end
end
def action(conn, module) do
with {:ok, user_id} <- Map.fetch(conn.private, :user_id),
%User{} = user <- Repo.get(User, user_id) do
apply(module, action_name(conn), [conn, conn.params, user])
else
_ ->
conn
|> put_status(:forbidden)
|> json(%{
error: "forbidden"
})
end
end
end