ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Elixirメタプログラミングで書く、関数っぽい関数でない何か

今回の 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

ここで readwritedelete といった関数は定義されていないことに注目です。 can? の第 2 引数は、「関数を呼び出した結果の値」ではなく「関数呼び出し」そのものなので、呼び出す関数自体の定義は必要ないからです。

また Elixir は、次のような関数呼び出しを、

func(arg1, arg2)

パイプ演算子 |> を使ってこのように記述することができます。

arg1 |> func(arg2)

そうすると、こんな表現も可能です。

クエスチョンマークの位置や括弧の存在が惜しいところですが。

if user |> can?(write(article)) do
  # do something
end

実は。 この仕組みのアイディアは、Elixir の Canada というパッケージから拝借しています。

github.com

Canada ではマクロの他にプロトコルという仕組みも利用していて、より柔軟に適用することができるようになっています。

そしてコードを読んで、記述できる表現に対してそのコードのコンパクトさに驚いてみてください。

ご注意

メタプログラミングは用法用量を守ることが必須です。 くれぐれも過剰摂取にはご注意を。

物以類聚

プログラムをメタプログラミングする行為には、不思議な魅力があります。

普通のプログラミングだけでは物足りないと感じている方、プログラムをメタプログラミングしたい方、プログラミング言語をプログラミングしたいと感じている方。 同志を探しに来てみませんか?

agile.esm.co.jp