LoginSignup
8
4

More than 5 years have passed since last update.

Elixir でモジュール属性を使って関数定義をデコレーションする

Posted at

Elixir: 1.5.1, Phoenix: 1.3.0


Python ではメソッドの前に @hoge を書いてメソッド定義の挙動を変えられる。

class Foo:
    def i_am_instance_method(self):
        print('hi')

    @staticmethod
    def i_am_static_method():
        print('yo')

Elixir で似たようなことをやるにはモジュール属性と @on_definition をうまく使う。

defmodule OnDefinition do
  def on_definition(env, _kind, _name, _args, _guards, _body) do
    Module.put_attribute(env.module, :my_func_attr, nil)
  end
end

defmodule Foo do
  # Foo モジュール内で関数を定義したタイミングで呼ばれるフックを指定する。
  @on_definition {OnDefinition, :on_definition}

  # これをモジュール定義の最初のほうで実行すると、
  # 以降このモジュール内に出現した `@my_func_attr expr` が記録されていく。
  # このモジュールのコンパイルフェーズ中に限り、
  # `Module.get_attribute(Foo, :my_func_attr)` マクロでリストとして取り出せる。
  # なお `@foo` と書くと `Module.get_attribute(__MODULE__, :foo)` に展開される。
  Module.register_attribute(Foo, :my_func_attr, accumulate: true)

  # 初期値として nil を入れておく。
  @my_func_attr nil

  ###

  def fn1() do
    # @my_func_attr 属性のリストから直前に定義されたものを取り出す。
    # 属性はリストの先頭に追加されていくため、直前のものを取り出すには List.first を呼ぶ。
    IO.puts inspect(unquote(@my_func_attr) |> List.first) # => nil
  end

  @my_func_attr "hello"
  def fn2() do
    # 直前に定義した属性の値を取り出せる。
    IO.puts inspect(unquote(@my_func_attr) |> List.first) # => "hello"
  end

  def fn3() do
    # fn3 の定義の前には @my_func_attr を置いていないが、
    # 上で fn2 を定義したタイミングで呼ばれる on_definition フックが
    # fn2 の直後に `@my_func_attr nil` を挿入しているため、ここでは nil が取り出される。
    IO.puts inspect(unquote(@my_func_attr) |> List.first) # => nil
  end
end

応用例

Phoenix のコントローラのアクション関数をデコレーションし、ログイン中のみアクセスできるページを実装できる。

defmodule MyApp.Auth do
  # ログイン状態でなければログインページにリダイレクトする plug。
  @spec require_authorization(Plug.Conn.t, Plug.opts) :: Plug.Conn.t
  def require_authorization(conn, _opts) do
    if MyApp.Session.signed_in?(conn) do
      conn
    else
      conn
      |> Phoenix.Controller.redirect(to: "/login")
      |> Plug.Conn.halt()
    end
  end

  # コントローラで関数を定義するたびに呼ばれる。関数の直前に `@protect true` があれば、
  # その関数にルーティングする前にログイン状態をチェックする plug を差し込む。
  def put_require_authorization_plug(env, _kind, name, _args, _guards, _body) do
    attrs = Module.get_attribute(env.module, :protect)

    protect_by_default? = List.last(attrs)
    protected? = List.first(attrs)

    if protected? do
      plug = {:require_authorization, [], quote(do: var!(action) == unquote(name))}
      Module.put_attribute(env.module, :plugs, plug)
    end

    Module.put_attribute(env.module, :protect, protect_by_default?)
  end

  @spec __using__([protect_by_default: boolean]) :: term
  defmacro __using__([protect_by_default: protect_by_default?]) do
    quote do
      import Worklog.Auth

      Module.register_attribute(__MODULE__, :protect, accumulate: true)

      @protect unquote(protect_by_default?)
      @on_definition {MyApp.Auth, :put_require_authorization_plug}
    end
  end
end

defmodule MyApp.FooController do
  use MyAppWeb, :controller
  use MyApp.Auth, protect_by_default: false

  # `@protect true` をつけていないので、アクセスにログインを要求しない。
  def index(conn, _params) do
    # ...
  end

  # `@protect true` をつけてあるので、アクセスにログインを要求する。
  # セッションにログイン状態が記録されていなければログインページにリダイレクトする。
  @protect true
  def edit(conn, params) do
    # ...
  end
end
8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4