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
が様々な型に適用できるのはこのプロとるを実装しているからだ。
ほかにもあるらしいけど、とりあえずこのへんで。