Elixir import/2 overrides a previously declared import/2
Elixir has import/2
form that imports some symbols in the current scope from other modules. It overrides previously declared import/2
. Let me show an example:
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
In the above example, you can see List.first/1
which was available at iex(3)
is undefined after import List, only: [flatten: 1]
at iex(4)
.
Postmortem
It’s a real story in my company’s production code. When I was writing a new Phoenix controller, I noticed that there were similar patterns in some controllers:
- Fetch the authenticated user’s ID from
conn.private
(Authentication is implemented in the another plug). - Proceed with the requested action if the user exists in the DB.
- Otherwise, return an error response.
I happened to feel like being a good programmer, I began to write a macro to reduce duplicate code.
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
Previously, a typical controller looks like:
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
It’s verbose and boring. With my new AuthUserAction
module, it should turn to be simpler.
use Phoenix.Controller
use AuthUserAction
def my_action(conn, params, user) do
...
end
Great! Job done, right?
Compiler Errors
But once I run mix compile
, the compiler complained about AuthUserAction
:
== 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
The reason why the compiler couldn’t find the function put_new_layout/2
was import/2
behavior I already described. The line below:
import Phoenix.Controller, only: [json: 2, action_name: 1]
This import/2
overrode the function put_new_layout/2
imported in use Phoenix.Controller
.
How to solved it?
Because import/2
is lexical, so we moved it into the action/2
function.
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
Now the compiler successfully compiles our code.
By the way, in the real production code, the most of implementation was moved into an ordinary function.
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