Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away