スポンサーリンク

2015年8月16日

[Elixir]マクロの簡単なまとめ

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

Goal

Elixirでメタプログラミングをするためのマクロを簡単にまとめる。
また、シンプルな例を作成する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4

Wait a minute

Elixirでもメタプログラミングがしたい!!
思い立ったのはいいんですが・・・
私の力だと・・・いまいち理解できない。
どういったように使っていけばいいのかも分からない。
これは、実際に利用しているソースコードを見ていくのが良いと思いますが。
quoteとunquoteの記事を上げた時から時間も経っています。
ここで一度、関数や機能の簡単なまとめをしようと思います。
Phoenix-Frameworkの作者で知られるChris McCord氏が著者である、
この本が欲しい・・・金欠な身が恨めしい。
本: Metaprogramming Elixir
知り合いが発注したらしいので、今度見せてもらおう!!(他力本願)

Index

Macro Overview
|> Brief summary of the function
|> A simple example
|> Extra

Brief summary of the function

ドキュメントやelixir-langのGetting Startの
関数や機能(quote、unquote)なんかを簡単にまとめます。
一覧・・・

Kernel.SpecialForms.quote/2

Description:
任意の式の表現を取得できる。
以下のようなタプルで返ってくる。
{function, meta-data, argument}
quoteのドキュメント長いですね(笑)
Example:
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
以下のようにすると少し複雑になるが、返ってくるタプルの形は変わらない。
(argumentの部分にされているだけ)
iex> quote do: (1 * 2) - 3 + 4
{:+, [context: Elixir, import: Kernel],
 [{:-, [context: Elixir, import: Kernel],
   [{:*, [context: Elixir, import: Kernel], [1, 2]}, 3]}, 4]}
少し形を変えて分かりやすくするとこんな感じになる。
+           {:+, [...],
|
|--- -        [{:-, [...],
| |
| |--- *        [{:*, [...],
| | |--- 1                  [1,
| | |--- 2                    2]},
| |
| |--- 3         3]},
|
|--- 4         4]}
この動画内で表示されているツリーを参考にしました。
参考: chrismccord>_ - NDC Oslo 2015 - Metaprogramming Elixir

Kernel.SpecialForms.unquote/1

Description:
マクロ内部で与えられた式(変数や関数)をUnquoteする。
マクロ内部で変数や関数をそのまま与えると、そのまま表現されてしまう。
変数の値もしくは関数の結果を必要としているのであれば、unquoteを利用する。
Example:
iex> x = 1
1
iex> quote do: x + x
{:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, {:x, [], Elixir}]}
iex> quote do: unquote(x) + unquote(x)
{:+, [context: Elixir, import: Kernel], [1, 1]}

Kernel.SpecialForms.unquote_splicing/1

Description:
引数へ拡張リストを与えた場合にUnquotesする。
unquoteに似ている。
変数にリストを束縛し、そのままリスト内で使おうとすると変数値として展開してくれない。
その場合には、unquote_splicingを利用する。
Example:
iex> x = [3, 4, 5]
[3, 4, 5]
iex> quote do: [1, 2, unquote(x), 6]
[1, 2, [3, 4, 5], 6]
iex> quote do: [1, 2, unquote_splicing(x), 6]
[1, 2, 3, 4, 5, 6]

Kernel.defmacro/2

Description:
指定された名前と内容のマクロを定義する。
Example:
iex> defmodule Math do
...>   defmacro add(x, y) do
...>     quote do
...>       unquote(x) + unquote(y)
...>     end
...>   end
...> end

iex> Math.add(1, 2)
** (CompileError) iex:2: you must require Math before invoking the macro Math.add/2
    (elixir) src/elixir_dispatch.erl:97: :elixir_dispatch.dispatch_require/6
iex> require Math
nil
iex> Math.add(1, 2)
3

Kernel.defmacrop/2

Description:
プライベートでマクロを定義する。
プライベートマクロは定義された同じモジュールからのみアクセス可能。

Macro.to_string/2

Description:
与えられた式をバイナリへ変換する。
Example:
iex> Macro.to_string(quote do: 1 + 2 + 3)
"1 + 2 + 3"

Macro.escape/2

Description:
再帰的に値を退避できる。
Example:
iex> x = %{hoge: :huge}
%{hoge: :huge}
iex> quote do: unquote(x)
%{hoge: :huge}
iex> Macro.escape(x)
{:%{}, [], [hoge: :huge]}

Macro.expand_once/2

Description:
AST(Abstract Syntax Tree)ノードを受け取り、一度だけそれを展開する。
式を展開できない場合は、式自体を返す。
再帰的に展開した場合は、Macro.expand/2を利用する。
対応している式。
  • マクロ (ローカル、リモート)
  • 展開できるalias
  • 擬似変数(ENVMODULEDIR)
  • reader属性のモジュール
Example:
iex> defmodule Math do
...>   defmacro add(x, y) do
...>     quote do
...>       unquote(x) + unquote(y)
...>     end
...>   end
...> end

iex> require Math
nil
iex> expr = quote do: Math.add(1, 2)
{{:., [], [{:__aliases__, [alias: false], [:Math]}, :add]}, [], [1, 2]}
iex> res = Macro.expand_once(expr, __ENV__)
{:+, [context: Math, import: Kernel], [1, 2]}
iex> IO.puts Macro.to_string(res)
1 + 2
:ok

Macro.var/2

Description:
アトムとコンテキストによって与えられた変数を表すAST(Abstract Syntax Tree)ノードを生成。
iex> Macro.var(:hoge, nil)
{:hoge, [], nil}

Kernel.var!/2

Description:
quoteする内部で使用する場合、変数が衛生にならないことをマークする。
(ドキュメントの翻訳は私の力だと上手く表現できないです。)
マクロ内で変数の束縛をした時に、マクロ外でも束縛を影響させることができます。
説明し辛いです・・・実例で動作を見てもらえれば、少しは分かると思います。
Example:
iex> defmodule Math do
...>   defmacro add(x, y) do
...>     quote do
...>       result = unquote(x) + unquote(y)
...>       var!(x) = result
...>       y = result
...>       result
...>     end
...>   end
...> end

iex> require Math
nil
iex> x = 1
1
iex> y = 2
2
iex> Math.add(x, y)
3
iex> x
3
iex> y
2
xの値は変わっていますが、yの値は変わっていません。
あまり使わない気もしますが・・・
マクロの基本で使いそうなのは、この当たりの機能や関数だと思います。
後は、ここら辺のドキュメントを見れば概ね問題ないと思う。
hexdocs - v1.0.5 Elixir Kernel
hexdocs - v1.0.5 Elixir Kernel.SpecialForms
hexdocs - v1.0.5 Elixir Macro

A simple example

elixir-langのGetting Startにあるサンプルコードを実施します。
Example:
ファイル: unless.exs
defmodule Unless do
  def fun_unless(clause, expression) do
    if(!clause, do: expression)
  end

  defmacro macro_unless(clause, expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end
実行してみましょう。
>iex unless.exs
iex> require Unless
nil
iex> Unless.macro_unless true, IO.puts "this should never be printed"
nil
iex> Unless.fun_unless true, IO.puts "this should never be printed"
this should never be printed
nil
Description:
関数呼び出しの引数は、関数を呼び出す前に評価される。
しかし、マクロは引数を評価しない。
そのため、macro_unless/2のマクロはIO.putsの内容が出力されていない。
定義したマクロは、以下のように展開されている。
引数は、quoteされた式に変換され、quoteされた式として受け取る。
macro_unless(true, {{:., [], [{:aliases, [], [:IO]}, :puts]}, [], ["this should never be printed"]})
つまり、先ほど定義したマクロは内部で書き換えられているわけですね。

Extra

Elixirでiexを使っている時に関数あったっけ?となることがある。
そんな時は、iexのhelper関数であるhを使おう!
モジュールを見てみる。
iex> h Macro
* Macro

Conveniences for working with macros.
モジュールにある関数を見てみる。
iex> h Macro.var
* def var(var, context)Genrates a AST node representing the variable givenby the atoms `var` and `context`.## ExamplesIn order to build a variable, a context is expected.Most of the times, in order to preserve hygiene, thecontext must be `__MODULE__`:    iex> Macro.var(:foo, __MODULE__)    {:foo, [], __MODULE__}However, if there is a need to access the user variable,
nil can be given:    iex> Macro.var(:foo, nil)    {:foo, [], nil}
存在しない関数を見てみる。
iex> h Macro.var!
No documentation for Macro.var! was found
どっかで書いたことある気がするけど・・・忘れているからいいや!

Speaking to oneself

今の自分の力だと、マクロを記事にするのはこれが限界でした。
本当はもっと華麗に展開するマクロを作っていく予定だったのに(冗談)
使い方は、どなたのGithubで公開されているソースコードでも探します。
また、何か分かったら記事にします。
しかし、なんかこう・・・メタプログラミングって聞くとわくわくしません?
そういえば、C++やってた時もTemplateの機能使いこなそうとして・・・
結局Boostライブラリを使っていた気が・・・(汗)
いや、今回こそはしっかりと習得したい。
Elixirでもメタプログラミングしたい!!
これが言いたかっただけ(笑)
Chris McCord氏の動画がすごい参考になる。
参考: chrismccord>_ - NDC Oslo 2015 - Metaprogramming Elixir
早く、PhoenixやEctoのソースコードを読めるようになりたいな。

Bibliography

人気の投稿