本稿はElixir公式サイトの許諾を得て「Protocols」の解説にもとづき、加筆補正を加えて、Elixirのプロトコルについてご説明します。プロトコルは、Elixirで多態性を実現する仕組みです。プロトコルが実装されてさえいれば、データは問わずに呼び出せます。
プロトコル
たとえば、String.Chars
プロトコルには、to_string/1
が備わっています。けれど、タプルはこのプロトコルを実装していません。
iex> to_string(1)
"1"
iex> to_string(:atom)
"atom"
iex> to_string({3.14, "apple", :pie})
** (Protocol.UndefinedError) protocol String.Chars not implemented for {3.14, "apple", :pie}. This protocol is implemented for: Atom, BitString, Date, DateTime, Float, Integer, List, NaiveDateTime, Time, URI, Version, Version.Requirement
(elixir) /private/tmp/elixir-20180507-68757-17gx35t/elixir-1.6.5/lib/elixir/lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) /private/tmp/elixir-20180507-68757-17gx35t/elixir-1.6.5/lib/elixir/lib/string/chars.ex:22: String.Chars.to_string/1
プロトコルの実装はdefimpl/3
で定められます。プロトコルのあとにfor:
で実装するデータを添え、do
ブロックに加えるのは呼び出す関数です。
defimpl String.Chars, for: Tuple do
def to_string(tuple) do
interior =
tuple
|> Tuple.to_list()
|> Enum.map(&Kernel.to_string/1)
|> Enum.join(", ")
"{#{interior}}"
end
end
-Tuple.to_list/1
: タプルをリストにします。
-Enum.map/2
: 列挙可能の項目を関数に渡し、戻り値のリストを返します。
-Enum.join/2
: 列挙可能の項目を文字列にして、第2引数の文字列でつなぎます。
iex> to_string({3.14, "apple", :pie})
"{3.14, apple, pie}"
Elixirではデータ構造の中に項目がいくつあるか調べるときに、つぎのふたつの慣用句があります。
-
length
: 計算して情報を得ます。- たとえば、
length/1
はリストの項目をすべて数えて返します。
- たとえば、
-
size
: データ構造の中にすでに計算された情報が含まれています。- たとえば、
tuple_size/1
やbyte_size/1
は、データの大きさにかかわらず値がすぐに取り出せます。
- たとえば、
Elixirには型ごとにサイズ(size
)を得る関数が組み込まれています。けれども、プロトコルを実装すれば、予め計算されたサイズをもつすべてのデータ構造からその値が得られるのです。
新たなプロトコルをつくるにはdefprotocol/2
を用います。あとに続くのはプロトコル名とdo
ブロックです。そのあとに、defimpl/3
で実装する関数を加えてください。もちろん、プロトコルが実装されていないデータ型を渡せばエラーが返ります。
defprotocol Size do
@doc "データ構造のサイズを求めます。"
def size(data)
end
defimpl Size, for: BitString do
def size(string), do: byte_size(string)
end
defimpl Size, for: Map do
def size(map), do: map_size(map)
end
defimpl Size, for: Tuple do
def size(tuple), do: tuple_size(tuple)
end
iex> Size.size("hello")
5
iex> Size.size({:hello, "world"})
2
iex> Size.size(%{hello: "world"})
1
iex> Size.size([:hello, "world"])
** (Protocol.UndefinedError) protocol Size not implemented for [:hello, "world"]
size.exs:1: Size.impl_for!/1
size.exs:3: Size.size/1
プロトコルはElixirのデータ型すべてに実装できます。
Atom
BitString
Float
Function
Integer
List
Map
PID
Port
Reference
Tuple
プロトコルと構造体
プロトコルを構造体とともに使うことでElixirの拡張性が増します。構造体は素のマップで、マップと同じプロトコルは実装していません。構造体として実装されているのはMapSet
です。
前項で定めたSize
プロトコルは、構造体には実装しませんでした。サイズを調べるにはMapSet.size/1
を用います。
iex> Size.size(%{})
0
iex> set = %MapSet{} = MapSet.new
#MapSet<[]>
iex> Size.size(set)
** (Protocol.UndefinedError) protocol Size not implemented for #MapSet<[]>
size.exs:1: Size.impl_for!/1
size.exs:3: Size.size/1
iex> MapSet.size(set)
0
MapSet.size/1
も予め計算されたサイズを返します。Size
プロトコルをMapSet
に実装しましょう。
defimpl Size, for: MapSet do
def size(set), do: MapSet.size(set)
end
iex> Size.size(%MapSet{})
0
構造体には使い途に応じたプロトコルが実装できます。
defmodule User do
defstruct [:name, :age]
end
defimpl Size, for: User do
def size(_user), do: 2
end
iex> john = %User{age: 27, name: "John"}
%User{age: 27, name: "John"}
iex> Size.size(john)
2
anyの実装
すべてのプロトコルをひとつひとつ実装するのは、手間がかかるでしょう。Elixirでこのようときには、ふたつやり方があります。第1に、その型のプロトコル実装を派生(derive)させることです。第2は、すべての型に自動的にプロトコルを実装することもできます。いずれの場合も、Any
にプロトコルを実装しなければなりません。
派生させる
Elixirでは、Any
の実装にもとづいて、プロトコルの実装を派生させることができます。
defimpl Size, for: Any do
def size(_), do: 0
end
このAny
のプロトコルを実装することが適当でない型もあります。そこで、この実装をさせたい構造体は、defstruct/1
の前に@derive
属性で明示的に派生させなければならないのです(「Deriving」参照)。
defmodule OtherUser do
@derive [Size]
defstruct [:name, :age]
end
iex> Size.size(%OtherUser{})
0
iex> Size.size([1, 2, 3])
** (Protocol.UndefinedError) protocol Size not implemented for [1, 2, 3]
example.exs:1: Size.impl_for!/1 example.exs:3: Size.size/1
anyにフォールバックする
実装がみつからないときに、プロトコルをAny
にフォールバックするよう明示することもできます。defimpl/3
のプロトコルの定めに、@fallback_to_any
属性をtrue
に設定するのです。こうすると、プロトコルを実装していないすべてのデータ型は、Any
の実装にしたがい、エラーは起こしません。
defprotocol Size do
@fallback_to_any true
def size(data)
end
defimpl Size, for: Any do
def size(_), do: 0
end
iex> Size.size([1, 2, 3])
0
ほとんどのプロトコルでは、実装されていないときエラーを返すのが適切とされています。そういう場合には、@derive
を使って明示する方がよいでしょう。Elixirの開発者は暗黙より明示を好むようです。多くのライブラリも@derive
を採用しています。
組み込み済みプロトコル
Elixirには予めいくつかのプロトコルが組み込まれています。そのうちのひとつはEnumerable
プロトコルです。このプロトコルを実装するデータ構造には、Enum
モジュールの関数が使えます。
iex> Enum.map([1, 2, 3], fn(x) -> x * x end)
[1, 4, 9]
iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
6
もうひとつ利用しやすい例はString.Chars
プルトコルです。文字を含むデータ構造が文字列に変換できます。to_string/1
関数として公開されています。
iex> to_string(:hello)
"hello"
Elixirの文字列補間はto_string/1
関数を呼び出すことにご注意ください。そして、数値はString.Chars
プルトコルを実装しています。
iex> age = 25
25
iex> "age: #{age}"
"age: 25"
けれど、タプルはString.Chars
プルトコルは実装していません。
iex> tuple = {1, 2, 3}
{1, 2, 3}
iex> "tuple: #{tuple}"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}
(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir) lib/string/chars.ex:22: String.Chars.to_string/1
複雑なデータ構造を出力したいときのために、Inspect
プロトコルに備わるのがinspect/2
関数です。
iex> "tuple: #{inspect(tuple)}"
"tuple: {1, 2, 3}"
Inspect
は任意のデータ構造を、読み取れるテキスト表現に変換するプロトコルです。IExなどのツールが結果を出力するのに用いられます。
iex> {1, 2, 3}
{1, 2, 3}
iex> %User{name: "john", age: 27}
%User{age: 27, name: "john"}
慣例として、出力された値が#
で始まるときは、Elixirの構文上有効でないデータ構造であることを示します。Inspect
プロトコルはもとに戻せず、情報は失われる場合があることを意味します。
iex> inspect &(&1+2)
"#Function<6.71889879/1 in :erl_eval.expr/5>"
プロトコルの統合
ElixirプロジェクトでビルドツールMixを使って作業していると、つぎのような出力をみることがあります。
Consolidated String.Chars
Consolidated Collectable
Consolidated List.Chars
Consolidated IEx.Info
Consolidated Enumerable
Consolidated Inspect
これらはElixirとともに提供されているすべてのプロトコルです。それらが統合されています。プロトコルは任意のデータ型に対して実行できるので、呼び出しのたびにその型が実装しているかどうか確かめなければなりません。すると、負荷は高まります。
けれど、Mixのようなツールを使ってコンパイルすれば、定義されたすべてのモジュールは、プロトコルやその実装も含めて明らかになります。こうして、プロトコルは統合され、むだなく実行の速いモジュールになるのです。
Elixir 1.2から、すべてのプロジェクトでプロトコルの統合が自動的に行われます。プロジェクトのビルドについては、Elixir official Mix & OTP Guideをお読みください。
Elixir入門もくじ
- Elixir入門 01: コードを書いて試してみる
- Elixir入門 02: 型の基本
- Elixir入門 03: 演算子の基本
- Elixir入門 04: パターンマッチング
- Elixir入門 05: 条件 - case/cond/if
- Elixir入門 06: バイナリと文字列および文字リスト
- Elixir入門 07: キーワードリストとマップ
- Elixir入門 08: モジュールと関数
- Elixir入門 09: 再帰
- Elixir入門 10: EnumとStream
- Elixir入門 11: プロセス
- Elixir入門 12: 入出力とファイルシステム
- Elixir入門 13: aliasとrequireおよびimport
- Elixir入門 14: モジュールの属性
- Elixir入門 15: 構造体
- Elixir入門 16: プロトコル
- Elixir入門 17: 内包表記
- Elixir入門 18: シギル
- Elixir入門 19: tryとcatchおよびrescue
- Elixir入門 20: 型の仕様とビヘイビア
- Elixir入門 21: デバッグ
- Elixir入門 22: Erlangライブラリ
- Elixir入門 23: つぎのステップ
Top comments (0)