エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Elixirの関数っぽい関数でない何かと、Prologの述語っぽい述語でない何か

Canada という小さな実装のライブラリがあります。

hex.pm

Ruby でいうところの CanCanCan のような権限判定のためのライブラリなのですが、とても興味深い実装をしています。

例えば userarticleread できるか判定するとき、

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/1update/1delete/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:_))) :- !.

GNU Prolog を起動します。

$ 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/1update/1delete/1 といった述語は定義していません。 加えて user/2article/3 も定義していません。 さらに言うと、Prolog には : という演算子は定義されていません。

Prolog は遅延評価であるため、明示的に評価するまで字面のまま扱われます。

そこで user(id: 123, role: reader) と言う記述は述語の定義の user(id:ID, role:_) にマッチし、変数 ID123 が束縛されます。 あとパタンマッチングによって can/2 の定義に適えば yes をそうでなければ no を返すと言うふるまいをします。

Elixir ではマクロという仕組みを使って「関数呼び出し」を引数として受け取れるようにしましたが、Prolog のばあいは逆に明示的に評価するまでは渡された引数の形のまま扱われるため、評価したときにどのような値が得られるかという定義がなくてもパタンマッチに利用できるという面白さがあります。