プロトコル

 cf. 16 プロトコル – Protocols – Elixir

プロトコル

プロトコルは Elixir で多態を生み出すための仕組みだ。プロトコルの処理は、どんなデータ型であれそのデータ型がプロトコルを実装していれば処理できる。つまりこれが多態ってわけだ。
プロトコルは次のように定義する。

iex(1)> defprotocol Blank do
...(1)>   @doc "Returns true if data is considered blank/empty"
...(1)>   def blank?(data)
...(1)> end
{:module, Blank,
 <<70, 79, 82, 49, 0, 0, 18, 36, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 181,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__protocol__, 1}}

そしてこのプロトコルをデータ型に実装する。

iex(2)> defimpl Blank, for: Integer do
...(2)>   def blank?(_), do: false
...(2)> end
{:module, Blank.Integer,
 <<70, 79, 82, 49, 0, 0, 6, 80, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}}
iex(3)> defimpl Blank, for: List do
...(3)>   def blank?([]), do: true
...(3)>   def blank?(_), do: false
...(3)> end
{:module, Blank.List,
 <<70, 79, 82, 49, 0, 0, 6, 100, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 211,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}}
iex(4)> defimpl Blank, for: Map do
...(4)>   def blank?(map), do: map_size(map) == 0
...(4)> end
{:module, Blank.Map,
 <<70, 79, 82, 49, 0, 0, 6, 140, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}}
iex(5)> defimpl Blank, for: Atom do
...(5)>   def blank?(false), do: true
...(5)>   def blank?(nil), do: true
...(5)>   def blank?(_), do: false
...(5)> end
{:module, Blank.Atom,
 <<70, 79, 82, 49, 0, 0, 6, 124, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 211,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}}

いま、IntegerListMapAtom について Blank プロトコルを実装した。必要であればほかの型にも実装する。
それじゃ、確かめてみよう。

iex(6)> Blank.blank?(0)
false
iex(7)> Blank.blank?([])
true
iex(8)> Blank.blank?([1, 2, 3])
false

文字列に対しては実装していないのでエラーになる。

iex(9)> Blank.blank?("hello")
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"
    iex:1: Blank.impl_for!/1
    iex:3: Blank.blank?/1

プロトコルと構造体

構造体はマップの拡張だけど、プロトコルの実装は共有していない。確かめるために、User 構造体を定義してみる。

iex(9)> defmodule User do
...(9)>   defstruct name: "john", age: 27
...(9)> end
{:module, User,
 <<70, 79, 82, 49, 0, 0, 8, 240, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 186,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, %User{age: 27, name: "john"}}

そして確かめる。

iex(10)> Blank.blank?(%{})
true
iex(11)> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}
    iex:1: Blank.impl_for!/1
    iex:3: Blank.blank?/1

(空の)マップに対しては true が返ってきているのに対して、User ではエラーになっている。エラーを避けるためには、User 構造体に対して Blank プロトコルを実装する必要がある(とはいえ、どういうときに true にするかわからないけど。この例ではすべて false かな)。

デフォルト実装

すべての型にプロトコルを実装するのは面倒なので、デフォルトの実装を与えておく方法がある。プロトコル定義の中で@fallback_to_anytrue に設定すると可能になる。

iex(11)> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}
    iex:1: Blank.impl_for!/1
    iex:3: Blank.blank?/1
iex(11)> defprotocol Blank do
...(11)>   @fallback_to_any true
...(11)>   def blank?(data)
...(11)> end
warning: redefining module Blank (current version defined in memory)
  iex:11

{:module, Blank,
 <<70, 79, 82, 49, 0, 0, 18, 40, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 136,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__protocol__, 1}}

あー、なんか Blank モジュールを再定義したんで warning が出てるな。大丈夫かな?とりあえず進めてみる。
とにかく、上のようにプロトコルを定義して、Any に対して実装すればいい。

iex(12)> defimpl Blank, for: Any do
...(12)>   def blank?(_), do: false
...(12)> end
{:module, Blank.Any,
 <<70, 79, 82, 49, 0, 0, 6, 68, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 207,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:__impl__, 1}}

これで、プロトコルを明示的に実装していないすべての型(構造体を含む)は、Blank.blank? に対して false を返すようになった。

iex(13)> Blank.blank?(%User{})
false

組み込みプロトコル

Elixir には組み込みのプロトコルがある。例えば Enumerable。これを実装しているデータ型には Enum モジュールの関数が使える。
また、String.Charsto_string/1 が様々な型に適用できるのはこのプロとるを実装しているからだ。
ほかにもあるらしいけど、とりあえずこのへんで。