16 プロトコル - Protocols

プロトコルはElixirで多態を生み出すための機構です.プロトコルの処理は,どんなデータ型に対しても,そのデータ型がそのプロトコルを実装さえしていれば処理できます.例を見てみましょう.

Protocols are a mechanism to achieve polymorphism in Elixir. Dispatching on a protocol is available to any data type as long as it implements the protocol. Let's see an example.

Elixirではfalsenilだけがfalseとして扱われます.他の全てはtrueと評価されます.アプリケーションによっては,それがブランクであると考えられるべきときに真偽値を返すようなblank?プロトコルを規定することが重要になるかもしれません.

In Elixir, only false and nil are treated as false. Everything else evaluates to true. Depending on the application, it may be important to specify a blank? protocol that returns a boolean for other data types that should be considered blank. For instance, an empty list or an empty binary could be considered blanks.

以下のようにプロトコルを定義できます:

We could define this protocol as follows:

defprotocol Blank do
  @doc "Returns true if data is considered blank/empty"
  def blank?(data)
end

このプロトコルはblank?と呼ばれる,一つの引数を取る関数が実装されることを期待しています.このプロトコルを別のElixirデータ型で以下のように実装してみましょう:

The protocol expects a function called blank? that receives one argument to be implemented. We can implement this protocol for different Elixir data types as follows:

# 整数は決してブランクにならない - Integers are never blank
defimpl Blank, for: Integer do
  def blank?(_), do: false
end

# 空リストのときだけブランクになる - Just empty list is blank
defimpl Blank, for: List do
  def blank?([]), do: true
  def blank?(_),  do: false
end

# 空のマップのときだけブランクになる - Just empty map is blank
defimpl Blank, for: Map do
  # Keep in mind we could not pattern match on %{} because
  # it matches on all maps. We can however check if the size
  # is zero (and size is a fast operation).
  def blank?(map), do: map_size(map) == 0
end

# アトムがfalseとnilのときだけブランクになる - Just the atoms false and nil are blank
defimpl Blank, for: Atom do
  def blank?(false), do: true
  def blank?(nil),   do: true
  def blank?(_),     do: false
end

元々ある全てのデータ型について同じようにしていきます.型はこれだけあります:

And we would do so for all native data types. The types available are:

  • Atom
  • BitString
  • Float
  • Function
  • Integer
  • List
  • Map
  • PID
  • Port
  • Reference
  • Tuple

今,手元でプロトコルが定義され,実装もしました.すると呼び出すことができます:

Now with the protocol defined and implementations in hand, we can invoke it:

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

プロトコルを実装していないデータ型を渡すとエラーが発生します:

Passing a data type that does not implement the protocol raises an error:

iex> Blank.blank?("hello")
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"

16.1 プロトコルと構造体 - Protocols and structs

Elixirの拡張性はプロトコルと構造体が一緒に使われたときに効果を発揮します.

The power of Elixir's extensibility comes when protocols and structs are used together.

前の章で,構造体はマップであるけれども,プロトコルの実装はマップと共有していないということを学びました.前の章にあったUserという構造体を定義してみましょう:

In the previous chapter, we have learned that although structs are maps, they do not share protocol implementations with maps. Let's define a User struct as in the previous chapter:

iex> defmodule User do
...>   defstruct name: "john", age: 27
...> end
{:module, User,
 <<70, 79, 82, ...>>, {:__struct__, 0}}

そして確かめます:

And then check:

iex> Blank.blank?(%{})
true
iex> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}

構造体は,マップとプロトコルの実装を共有するのではなく,自身でプロトコルを実装することを求めます.

Instead of sharing protocol implementation with maps, structs require their own protocol implementation:

defimpl Blank, for: User do
  def blank?(_), do: false
end

必要に応じて,ユーザーがブランクであるという独自の意味を持たせることができます.それだけではなく,構造体をもっと堅牢な,たとえばキューのようなデータ型を構築するのに使うことができます,そしてそのようなデータ型のために,関連した全てのプロトコル,例えばEnumerableやおそらくBlankを実装する必要があります.

If desired, you could come up with your own semantics for a user being blank. Not only that, you could use structs to build more robust data types, like queues, and implement all relevant protocols, such as Enumerable and possibly Blank, for this data type.

わかりきったプロトコル実装を全ての構造体へ行うのは面倒ですから.開発者は手動で実装していない構造体へ使うデフォルト実装を用意したくなります,

In many cases though, developers may want to provide a default implementation for structs, as explicitly implementing the protocol for all structs can be tedious. That's when falling back to Any comes in handy.

16.2 何かに立ち戻る - Falling back to Any

全ての型のためのデフォルトの実装が用意されていれば便利ですよね.プロトコル定義の中で@fallback_to_anytrueに設定するとできるようになります:

It may be convenient to provide a default implementation for all types. This can be achieved by setting @fallback_to_any to true in the protocol definition:

defprotocol Blank do
  @fallback_to_any true
  def blank?(data)
end

そしてこのように実装できます:

Which can now be implemented as:

defimpl Blank, for: Any do
  def blank?(_), do: false
end

今,Blankプロトコルを実装していない,全ての(構造体を含む)データ型は「ブランクではない」と考えられることになりました.

Now all data types (including structs) that we have not implemented the Blank protocol for will be considered non-blank.

16.3 組み込みプロトコル - Built-in protocols

Elixirにはいくつか組み込みのプロトコルがあります.以前の章でEnumerableプロトコルを実装しているならどんなデータ構造であっても動作する関数を沢山用意しているEnumモジュールについてお話ししましたね:

Elixir ships with some built-in protocols. In previous chapters, we have discussed the Enum module which provides many functions that work with any data structure that implements the Enumerable protocol:

iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end
[2,4,6]
iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end
6

その他便利な例として,文字のあるデータ構造をどのように文字列に変換するかを規定したString.Charsプロトコルがあります,関数to_stringで使っています:

Another useful example is the String.Chars protocol, which specifies how to convert a data structure with characters to a string. It's exposed via the to_string function:

iex> to_string :hello
"hello"

Elixirの埋め込み文字列でも関数to_stringを呼んでいることを覚えておいてください:

Notice that string interpolation in Elixir calls the to_string function:

iex> "age: #{25}"
"age: 25"

上の例は数値がString.Charsプロトコルを実装しているの動作しています.例えばタプルを渡すとエラーになります:

The snippet above only works because numbers implement the String.Chars protocol. Passing a tuple, for example, will lead to an error:

iex> tuple = {1, 2, 3}
{1, 2, 3}
iex> "tuple: #{tuple}"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}

もっと複雑な"表示"が必要な場合,単にInspectプロトコルによってできている関数inspectを利用するという方法もあります:

When there is a need to "print" a more complex data structure, one can simply use the inspect function, based on the Inspect protocol:

iex> "tuple: #{inspect tuple}"
"tuple: {1, 2, 3}"

Inspectプロトコルはデータ構造を読むことができるテキスト表現へと変換します.IExで結果を表示するのにも使っています:

The Inspect protocol is the protocol used to transform any data structure into a readable textual representation. This is what tools like IEx use to print results:

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

評価された値が#から始まるときは,エリクサーの構文ではない方法でデータ構造を表しているという意味を表すならわしになっていることを覚えておいてください.つまり検査したプロトコルの情報が一部失われることがあるためにその情報からは元に戻せないということです:

Keep in mind that, by convention, whenever the inspected value starts with #, it is representing a data structure in non-valid Elixir syntax. This means the inspect protocol is not reversible as information may be lost along the way:

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

Elixirには他のプロトコルもありますが,最も共通しているものについてはカバーしました.次の章ではElixirにおいての例外とエラーハンドリングについて学びましょう.

There are other protocols in Elixir but this covers the most common ones. In the next chapter we will learn a bit more about error handling and exceptions in Elixir.