Elixir入門 16: プロトコル
Tags: elixir, webdev, tutorial, programming
Published: 2019-01-08 01:56:12

---
title: Elixir入門 16: プロトコル
published: true
description: Elixir公式サイトの許諾を得て「Protocols」の解説にもとづき、加筆補正を加えて、Elixirのプロトコルについてご説明します。プロトコルは、Elixirで多態性を実現する仕組みです。プロトコルが実装されてさえいれば、データは問わずに呼び出せます。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Protocols](https://elixir-lang.org/getting-started/protocols.html)」の解説にもとづき、加筆補正を加えて、Elixirのプロトコルについてご説明します。プロトコルは、Elixirで多態性を実現する仕組みです。プロトコルが実装されてさえいれば、データは問わずに呼び出せます。


# プロトコル
たとえば、[`String.Chars`プロトコル](https://hexdocs.pm/elixir/String.Chars.html)には、[`to_string/1`](https://hexdocs.pm/elixir/String.Chars.html#to_string/1)が備わっています。けれど、タプルはこのプロトコルを実装していません。

```elixir
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`](https://hexdocs.pm/elixir/Kernel.html#defimpl/3)で定められます。プロトコルのあとに`for:`で実装するデータを添え、`do`ブロックに加えるのは呼び出す関数です。

```elixir
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`](https://hexdocs.pm/elixir/Tuple.html#to_list/1): タプルをリストにします。
-[`Enum.map/2`](https://hexdocs.pm/elixir/Enum.html#map/2): 列挙可能の項目を関数に渡し、戻り値のリストを返します。
-[`Enum.join/2`](https://hexdocs.pm/elixir/Enum.html#join/2): 列挙可能の項目を文字列にして、第2引数の文字列でつなぎます。

```elixir
iex> to_string({3.14, "apple", :pie})
"{3.14, apple, pie}"
```

Elixirではデータ構造の中に項目がいくつあるか調べるときに、つぎのふたつの慣用句があります。

- `length`: 計算して情報を得ます。
    - たとえば、[`length/1`](https://hexdocs.pm/elixir/Kernel.html#length/1)はリストの項目をすべて数えて返します。
- `size`: データ構造の中にすでに計算された情報が含まれています。
    - たとえば、[`tuple_size/1`](https://hexdocs.pm/elixir/Kernel.html#tuple_size/1)や[`byte_size/1`](https://hexdocs.pm/elixir/Kernel.html#byte_size/1)は、データの大きさにかかわらず値がすぐに取り出せます。

Elixirには型ごとにサイズ(`size`)を得る関数が組み込まれています。けれども、プロトコルを実装すれば、予め計算されたサイズをもつすべてのデータ構造からその値が得られるのです。

新たなプロトコルをつくるには[`defprotocol/2`](https://hexdocs.pm/elixir/Kernel.html#defprotocol/2)を用います。あとに続くのはプロトコル名と`do`ブロックです。そのあとに、`defimpl/3`で実装する関数を加えてください。もちろん、プロトコルが実装されていないデータ型を渡せばエラーが返ります。

```elixir
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
```

```elixir
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`](https://hexdocs.pm/elixir/MapSet.html)です。

前項で定めた`Size`プロトコルは、構造体には実装しませんでした。サイズを調べるには[`MapSet.size/1`](https://hexdocs.pm/elixir/MapSet.html#size/1)を用います。

```elixir
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`に実装しましょう。

```elixir
defimpl Size, for: MapSet do
  def size(set), do: MapSet.size(set)
end
```

```elixir
iex> Size.size(%MapSet{})
0
```

構造体には使い途に応じたプロトコルが実装できます。

```elixir
defmodule User do
  defstruct [:name, :age]
end
defimpl Size, for: User do
  def size(_user), do: 2
end
```

```elixir
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`の実装にもとづいて、プロトコルの実装を派生させることができます。

```elixir
defimpl Size, for: Any do
  def size(_), do: 0
end
```

この`Any`のプロトコルを実装することが適当でない型もあります。そこで、この実装をさせたい構造体は、`defstruct/1`の前に`@derive`属性で明示的に派生させなければならないのです(「[Deriving](https://hexdocs.pm/elixir/Kernel.html#defstruct/1-deriving)」参照)。

```elixir
defmodule OtherUser do
  @derive [Size]
  defstruct [:name, :age]
end
```

```elixir
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`の実装にしたがい、エラーは起こしません。

```elixir
defprotocol Size do
  @fallback_to_any true
  def size(data)
end

defimpl Size, for: Any do
  def size(_), do: 0
end
```

```elixir
iex> Size.size([1, 2, 3])
0
```

ほとんどのプロトコルでは、実装されていないときエラーを返すのが適切とされています。そういう場合には、`@derive`を使って明示する方がよいでしょう。Elixirの開発者は暗黙より明示を好むようです。多くのライブラリも`@derive`を採用しています。



# 組み込み済みプロトコル
Elixirには予めいくつかのプロトコルが組み込まれています。そのうちのひとつは[`Enumerable`プロトコル](https://hexdocs.pm/elixir/Enumerable.html)です。このプロトコルを実装するデータ構造には、[`Enum`モジュール](https://hexdocs.pm/elixir/Enum.html)の関数が使えます。

```elixir
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`プルトコル](https://hexdocs.pm/elixir/String.Chars.html)です。文字を含むデータ構造が文字列に変換できます。`to_string/1`関数として公開されています。

```elixir
iex> to_string(:hello)
"hello"
```

Elixirの文字列補間は`to_string/1`関数を呼び出すことにご注意ください。そして、数値は`String.Chars`プルトコルを実装しています。

```elixir
iex> age = 25
25
iex> "age: #{age}"
"age: 25"
```

けれど、タプルは`String.Chars`プルトコルは実装していません。

```elixir
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`プロトコル](https://hexdocs.pm/elixir/Inspect.html)に備わるのが[`inspect/2`関数](https://hexdocs.pm/elixir/Inspect.html#inspect/2)です。

```elixir
iex> "tuple: #{inspect(tuple)}"
"tuple: {1, 2, 3}"
```

`Inspect`は任意のデータ構造を、読み取れるテキスト表現に変換するプロトコルです。IExなどのツールが結果を出力するのに用いられます。

```elixir
iex> {1, 2, 3}
{1, 2, 3}
iex> %User{name: "john", age: 27}
%User{age: 27, name: "john"}
```

慣例として、出力された値が`#`で始まるときは、Elixirの構文上有効でないデータ構造であることを示します。`Inspect`プロトコルはもとに戻せず、情報は失われる場合があることを意味します。

```elixir
iex> inspect &(&1+2)
"#Function<6.71889879/1 in :erl_eval.expr/5>"
```


# プロトコルの統合
Elixirプロジェクトでビルドツール[Mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html)を使って作業していると、つぎのような出力をみることがあります。

```terminal
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](https://github.com/arafatm/edu_elixir#elixir-official-mix--otp-guide)をお読みください。


#### Elixir入門もくじ
- [Elixir入門 01: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- [Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- [Elixir入門 10: EnumとStream](https://dev.to/gumi/elixir-10-enumstream-4fpb)
- [Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)
- [Elixir入門 12: 入出力とファイルシステム](https://dev.to/gumi/elixir-12--4og6)
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- [Elixir入門 15: 構造体](https://dev.to/gumi/elixir-15--4f43)
- Elixir入門 16: プロトコル
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- [Elixir入門 19: tryとcatchおよびrescue](https://dev.to/gumi/elixir-19-trycatchrescue-50i8)
- [Elixir入門 20: 型の仕様とビヘイビア](https://dev.to/gumi/elixir-20--j50)
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)