Elixir の import/2 は最後のものが優先される

>[個人ブログにも転載しています](https://metareal.blog/post/2020/03/26/elixir-import-overrides-previous-import/)

Elixir の [`import/2`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#import/2) を同じモジュールにたいして複数回実行すると、最後に実行したものが優先される。

```elixir
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 にあれば処理を続行、そうでなければエラー」という処理が頻発するので、次のようなモジュールを書いた。

```elixir
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
```

これを使うコントローラーでは、毎回、

```elixir
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` を追加するだけで、

```elixir
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
```

何故こうなるか、というと、最初に説明した通り、マクロ中の

```elixir
import Phoenix.Controller, only: [json: 2, action_name: 1]
```

が、コントローラー側の `use Phoenix.Controller` で `import` されてる `put_new_layout/2` を上書きしてしまうからだ。

### 解決

Elixir の `import/2` はレキシカル・スコープなので、import を関数内に移動するだけで解決できる。

```elixir
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 の実装をマクロの外に出した。

```elixir
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
```