今回の Elixir のお題には、メタプログラミングを選んでみました。
Elixir に限らずメタプログラミングでは、ふだん関数を書くときとは異なるレベルの視点が要求されます。
今回は、関数のように見えるけれど関数でないものをメタプログラミングで表現してみましょう。
例題
例として。 利用者 (User) が、記事 (Article) の読み書き削除の操作を可能かどうか、可否の判定をする仕組みを考えます。
読み書き削除の操作の可否は、次のように定義することにしました。
- 任意の User は Article を読むことができる
- User が Article の所有者、もしくはロールが editor の User のばあいは、Article に書き込むことができる
- User が Article の所有者のばあいは、Article を削除することができる
- それ以外の操作はできない
これをコードで表現していきます。
プログラミング Elixir
まず、利用者 User と記事 Article を表現する構造体を定義します。
User には、ID とロールを持たせます。
defmodule User do defstruct [:id, :role] end
Article は、所有者を識別するオーナ ID と内容を持つものとします。
defmodule Article do defstruct [:owner_id, :content] end
これらの構造体に対し、操作の判定をする関数は次のように定義しました。
操作は、第 3 引数のアトム(Ruby で言うところのシンボル)で指定しています。
defmodule Permission do def permitted?(%User{}, %Article{}, :read ), do: true # 任意のユーザは読み込み可能 def permitted?(%User{role: :editor}, %Article{}, :write ), do: true # 編集者は書き込み可能 def permitted?(%User{id: id}, %Article{owner_id: id}, :write ), do: true # 所有者は書き込み可能 def permitted?(%User{id: id}, %Article{owner_id: id}, :delete), do: true # 所有者は削除可能 def permitted?(%User{}, %Article{}, _ ), do: false # それ以外の操作は不可 end
ちなみに、%User{id: 123, role: :editor}
という表記は、Elixir の構造体のデータ表現です。
試してみましょう。
User が所有者のばあいは、すべての操作が可能です。
article = %Article{owner_id: 123} user = %User{id: 123} Permission.permitted?(user, article, :read) #=> true Permission.permitted?(user, article, :write) #=> true Permission.permitted?(user, article, :delete) #=> true
所有者でなくても、ロールが editor
のばあいは read
, write
が可能です。
しかし delete
はできません。
article = %Article{owner_id: 123} user = %User{id: 456, role: :editor} Permission.permitted?(user, article, :read) #=> true Permission.permitted?(user, article, :write) #=> true Permission.permitted?(user, article, :delete) #=> false
それ以外の User は、read
は可能ですが、write
, delete
はできません。
article = %Article{owner_id: 123} user = %User{id: 789} Permission.permitted?(user, article, :read) #=> true Permission.permitted?(user, article, :write) #=> false Permission.permitted?(user, article, :delete) #=> false
ここまでが普通の Elixir プログラミング。
これより先がメタプログラミングの世界です。
メタプログラミング Elixir
Elixir のメタプログラミングは、マクロという仕組みを利用します。
マクロを読む
普通の関数が入力として値を受け取り出力として値を返すのに対し、マクロは入力としてプログラムを受け取り出力としてプログラムを返す仕組みと言えます。
このときの入力と出力は、いわゆる AST (Abstract Syntax Tree) で表現されています。
たとえば文字列を出力する次のコードは、
IO.puts("Hi!")
このような表現になります。
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["Hi!"]}
(実際には、メタデータとしてファイル内の行番号などの情報が付加されるので、これとまったく同じ表現になるわけではないのですが、プログラム構造の部分のみを抜き出した骨格はこれと同じ形になります)
これを返すマクロを書いてみます。
defmodule Greeting do # 関数を定義する def ではなく、マクロを定義する defmacro を使う defmacro hi do {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["Hi!"]} end end
マクロを有効にするには、モジュールを require
もしくは import
します。
require Greeting Greeting.hi()
Greeting.hi()
の戻り値は IO.puts("Hi!")
の AST でしたので、結果として IO.puts("Hi!")
が実行されます。
実行するとコンソールに Hi!
と表示されると思います。
import
するとモジュール名の修飾が不要になるので呼び出しが少し簡単になりますが、名前の衝突には気をつけてください。
import Greeting hi()
マクロを使うとコードを差し込めることは分かりました。 しかし人の力で AST の表現のデータを記述するのは大変です。
そこで quote
を利用します。
quote
は、Elixir のコードを AST の表現に変換してくれます。
quote do IO.puts("Hi!") end #=> {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["Hi!"]}
また quote
の中で unquote
を利用すると、渡した変数を、変数の AST の表現でなく、変数の値として解釈してくれます。
quote do IO.puts(message) end #=> {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], [{:message, [], Elixir}]} # 「変数 message」として扱われる message = "Hello" quote do IO.puts(unquote(message)) end #=> {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["Hello"]} # 変数 message の値の「"Hello"」として扱われる
戻り値だけでなく、マクロの引数も AST の表現になっています。 マクロの引数の中で関数呼び出しを書くと、マクロは関数呼び出しの AST 表現を受け取ることになります。
article = %Article{owner_id: 123} quote do read(unquote(article)) end #=> {:read, [], [%Article{owner_id: 123, content: nil}]}
マクロの引数で「関数呼び出し」を受けると、マクロの中では「関数名」と「その関数の引数」に分解して利用できるわけです。
「関数名」がただの値になるなら、それを引数として関数を呼び出すことも可能なはずです。
これらを踏まえて。
can?(user, read(article))
と書くと permitted?(user, article, :read)
が実行されるマクロを書いてみましょう。
マクロを書く
Permission
モジュールに can?
マクロを追加します。
defmodule Permission do # permitted? 関数の定義は同じなので省略 defmacro can?(user, {action, _metadata, [article]}) do quote do Permission.permitted?(unquote(user), unquote(article), unquote(action)) end end end
第 2 引数では「関数呼び出し」の AST を、関数名とメタデータと関数の引数に分解してして受け取ります。 ここではメタデータは利用しません。
分解した関数名と引数を Permission.permitted?
の第 3 引数、第 2 引数に渡します。
このとき、「変数」ではなく「変数の値」を渡したいので unquote
することを忘れてはいけません。
使ってみましょう。
import Permission owner = %User{id: 123} editor = %User{id: 456, role: :editor} another_user = %User{id: 789} article = %Article{owner_id: 123} can?(owner, read(article)) #=> true can?(owner, write(article)) #=> true can?(owner, delete(article)) #=> true can?(editor, read(article)) #=> true can?(editor, write(article)) #=> true can?(editor, delete(article)) #=> false can?(another_user, read(article)) #=> true can?(another_user, write(article)) #=> false can?(another_user, delete(article)) #=> false
ここで read
や write
や delete
といった関数は定義されていないことに注目です。
can?
の第 2 引数は、「関数を呼び出した結果の値」ではなく「関数呼び出し」そのものなので、呼び出す関数自体の定義は必要ないからです。
また Elixir は、次のような関数呼び出しを、
func(arg1, arg2)
パイプ演算子 |>
を使ってこのように記述することができます。
arg1 |> func(arg2)
そうすると、こんな表現も可能です。
クエスチョンマークの位置や括弧の存在が惜しいところですが。
if user |> can?(write(article)) do # do something end
実は。 この仕組みのアイディアは、Elixir の Canada というパッケージから拝借しています。
Canada ではマクロの他にプロトコルという仕組みも利用していて、より柔軟に適用することができるようになっています。
そしてコードを読んで、記述できる表現に対してそのコードのコンパクトさに驚いてみてください。
ご注意
メタプログラミングは用法用量を守ることが必須です。 くれぐれも過剰摂取にはご注意を。
物以類聚
プログラムをメタプログラミングする行為には、不思議な魅力があります。
普通のプログラミングだけでは物足りないと感じている方、プログラムをメタプログラミングしたい方、プログラミング言語をプログラミングしたいと感じている方。 同志を探しに来てみませんか?