スポンサーリンク

2016年5月10日

[Elixir]Play with the Elixir-Macro

とある錬金術師の万能薬(Elixir)

Goal

  • マクロのさび落とし
  • 見た目の記法だけChankoっぽいもの(偽物)を実装する(内部実装は・・・)

Dev-Environment

  • OS: Windows8.1
  • Erlang: Eshell V7.3, OTP-Version 18.3
  • Elixir: v1.2.3

Play with the Elixir-Macro

A/Bテストをやろうと思っていたのに、気が付いたらマクロでやっていました。
(機能の一部で)見た目の記述だけChankoっぽいもの(完全な偽物)を実装してみました。
あくまで使うときの見た目だけなので、実装に関しては残念ながら微妙です。
結局、内部でtry~rescueとifを使って処理しています。
本気でやるならブラッシュアップが必要ですね。
最初のとっかかりとしては、まぁこんなもんでしょう。(残当)
それでは、やっていきます。

Module: Unit

テスト用のモジュールで使うマクロを定義したモジュールです。
  • active_if/1: 実行するのかしないのかのフラグを返すだけの関数をマクロで展開しているだけです。
  • function/2: 関数を動的にマクロで展開しているだけです。func_nameが関数名になります。

File: unit.ex

defmodule Unit do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
    end
  end

  defmacro active_if(condition) do
    quote do
      def active? do
        unquote(condition)
      end
    end
  end

  defmacro function(func_name, clauses) do
    if func_name do
      quote do
        def unquote(String.to_atom "#{func_name}")() do
          Keyword.get(unquote(clauses), :do, nil)
        end
      end
    end
  end
end

Module: TestControllerUnit

サンプルとしてUnitモジュールを使ってみたモジュールです。
Unitモジュールを実際に使ってみるとこんな感じになります。

File: test_controller_unit.ex

defmodule TestControllerUnit do
  use Unit

  active_if true

  function(:show) do
    d = 5
    e = d + 6
    f = e + 7
  end
end

Module: Helper

補助関数を定義しているモジュールです。
  • exec/2, exec/3, atom_to_module_string/1: モジュールがatomのままだとapplyで実行できないため、補助関数として作りました。

File: helper.ex

defmodule Helper do
  def exec(module, fun) do
    exec(module, fun, [])
  end
  def exec(module, fun, []) do
    atom_to_module_string(module) |> String.to_atom |> apply(fun, [])
  end
  def exec(module, fun, args) when is_list(args) do
    atom_to_module_string(module) |> String.to_atom |> apply(fun, args)
  end

  def atom_to_module_string(atom) do
    "Elixir." <> (Atom.to_string(atom) |> Mix.Utils.camelize)
  end
end

Module: Invoker

結局、内部でtry~rescueとifを使って、
実コードにてマクロが展開されているだけになってしまいました。
  • invoke/3: テスト対象のモジュールと関数を指定して、デフォルトの処理をdo~endで記述する。

File: invoker.ex

defmodule Invoker do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      import Helper
    end
  end

  defmacro invoke(module_name, func_name, clauses) do
    quote do
      do_clause = Keyword.get(unquote(clauses), :do, nil)

      try do
        if exec(unquote(module_name), :active?) do
          exec(unquote(module_name), unquote(func_name))
        else
          do_clause
        end
      rescue
        _any -> do_clause
      end
    end
  end
end

Module: TestController

サンプルとしてInvokerモジュールを使ってみたモジュールです。
Invokerモジュールを実際に使ってみるとこんな感じになります。

File: test_controller.ex

defmodule TestController do
  use Invoker

  def show do
    invoke(:test_controller_unit, :show) do
      a = 1
      b = a + 2
      c = b + 3
    end
  end
end

Example:

## active_if: true
iex> TestController.show
18

## active_if: false
iex> TestController.show
6
TestControllerUnitでraiseを起こしてみると、rescueの処理が実行される。

Example:

defmodule TestControllerUnit do
  ...

  function(:show) do
    d = 5
    e = d + 6
    raise "oops"
    f = e + 7
  end
end
iex> TestController.show
6
もう少しtry~rescueやifのあたりを何とかしたかったのですが、
マクロの動作に翻弄されて、そこまで手が回りませんでした。
あまり参考になるコードではないと思いますが、誰かの役に立ったなら幸いです。m( )m

Bibliography

人気の投稿