Canada という小さな実装のライブラリがあります。
Ruby でいうところの CanCanCan のような権限判定のためのライブラリなのですが、とても興味深い実装をしています。
例えば user
が article
を read
できるか判定するとき、
can?(user, read(article))
あるいは
user |> can?(read(article))
のような書き方をするのですが、このとき read/1
という関数は定義しません。
定義する必要がありませんというのがより正しいかもしれません。
何をやっているのか、その仕組みをなぞるコードを書いて確認してみましょう。
Elixir のばあい
まず、構造体 User と Article を定義します。
defmodule User do defstruct [:id, :role, :name] end
defmodule Article do defstruct [:user_id, :title, :body] end
次に判定のための関数 available?/3
を用意します。
この関数は User の値と Article の値、および atom で操作を受け取り、その組み合わせで操作の可否を返します。
ここでは任意の User は Article を read でき、role が editor である User あるいは所有者である User は Article を更新でき、所有者である User は Article を削除できる、としています。 それ以外の操作はできません。
User の種類 | read | write | delete |
---|---|---|---|
任意の User | 可 | 不可 | 不可 |
編集者 (role = editir) | 可 | 可 | 不可 |
所有者 (User.id = Article.user_id) | 可 | 可 | 可 |
defmodule Can do def available?(%User{}, :read, %Article{}), do: true def available?(%User{role: :editor}, :update, %Article{}), do: true def available?(%User{id: id}, :update, %Article{user_id: id}), do: true def available?(%User{id: id}, :delete, %Article{user_id: id}), do: true def available?(%User{}, _, %Article{}), do: false # 後半に続く
最後に、マクロ can?/2
を定義します。
ここで第 2 引数は「関数呼び出し」を受け取るようにします。
関数を呼び出した結果ではなく、関数呼び出しそのものを受け取るという点が要点です。
マクロでは関数呼び出しは関数名と引数に分解されます。
iex> quote do: read(foo) {:read, [], [{:foo, [], Elixir}]}
マクロの引数に関数呼び出しを渡すと、この分解された形で受け取ることになるので、分解された関数名と引数を使って available?/3
を評価します。
# 前半からの続き defmacro can?(user, {action, _, [article]}) do quote do available?(unquote(user), unquote(action), unquote(article)) end end end
マクロを有効にするために import して判定をしてみます。
import Can # 任意の User user = %User{id: 123} # 編集者 editor = %User{id: 234, role: :editor} # 所有者 owner = %User{id: 345} article = %Article{user_id: 345} user |> can?(read(article)) #=> true user |> can?(update(article)) #=> false user |> can?(delete(article)) #=> false editor |> can?(read(article)) #=> true editor |> can?(update(article)) #=> true editor |> can?(delete(article)) #=> false owner |> can?(read(article)) #=> true owner |> can?(update(article)) #=> true owner |> can?(delete(article)) #=> true
read/1
や update/1
や delete/1
といった関数の呼び出しが現れますが、それらを呼び出した結果でなく呼び出しそのものがマクロの引数となるため、関数の定義は存在しないという興味深い実装になっています。
Canada ではさらに available?/3
に相当する部分がプロトコルで実現されているために、任意の構造体に対して判定を定義することが可能になっています。
Prolog のばあい
同じようなことを Prolog でも書いてみました。
Elixir の母体である Erlang は最初は Prolog で書かれ Prolog の影響を受けていることは知られています。 実際 Prolog で何が起こるか見てみることで、似ているところ違うところを感じてみましょう。
次のコードを can.prolog
と言うファイル名で保存します。
can(user(id:_, role:_), read(article(user_id:_, title:_, body:_))) :- !. can(user(id:_, role:editor), update(article(user_id:_, title:_, body:_))) :- !. can(user(id:ID, role:_), update(article(user_id:ID, title:_, body:_))) :- !. can(user(id:ID, role:_), delete(article(user_id:ID, title:_, body:_))) :- !.
$ gprolog
Prolog のプロンプトが表示されたら ['can.prolog'].
と入力してコードを読み込みます。
| ?- ['can.prolog']. yes
Elixir で書いた時と同じように、任意の User、編集者、所有者それぞれに対して read, update, delete が可能か判定させてみます。
| ?- can(user(id: 123, role: reader), read(article(user_id: 345, title: "Foo", body: "Bar"))). yes | ?- can(user(id: 123, role: reader), update(article(user_id: 345, title: "Foo", body: "Bar"))). no | ?- can(user(id: 123, role: reader), delete(article(user_id: 345, title: "Foo", body: "Bar"))). no | ?- can(user(id: 234, role: editor), read(article(user_id: 345, title: "Foo", body: "Bar"))). yes | ?- can(user(id: 234, role: editor), update(article(user_id: 345 title: "Foo", body: "Bar"))). yes | ?- can(user(id: 234, role: editor), delete(article(user_id: 345, title: "Foo", body: "Bar"))). no | ?- can(user(id: 345, role: reader), read(article(user_id: 345, title: "Foo", body: "Bar"))). yes | ?- can(user(id: 345, role: reader), update(article(user_id: 345, title: "Foo", body: "Bar"))). yes | ?- can(user(id: 345, role: reader), delete(article(user_id: 345, title: "Foo", body: "Bar"))). yes
同じように判定することができました。
ここで read/1
や update/1
や delete/1
といった述語は定義していません。
加えて user/2
や article/3
も定義していません。
さらに言うと、Prolog には :
という演算子は定義されていません。
Prolog は遅延評価であるため、明示的に評価するまで字面のまま扱われます。
そこで user(id: 123, role: reader)
と言う記述は述語の定義の user(id:ID, role:_)
にマッチし、変数 ID
に 123
が束縛されます。
あとパタンマッチングによって can/2
の定義に適えば yes をそうでなければ no を返すと言うふるまいをします。
Elixir ではマクロという仕組みを使って「関数呼び出し」を引数として受け取れるようにしましたが、Prolog のばあいは逆に明示的に評価するまでは渡された引数の形のまま扱われるため、評価したときにどのような値が得られるかという定義がなくてもパタンマッチに利用できるという面白さがあります。