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}}
いま、Integer
、List
、Map
、Atom
について 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_any
を 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
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.Chars
。to_string/1
が様々な型に適用できるのはこのプロとるを実装しているからだ。
ほかにもあるらしいけど、とりあえずこのへんで。