layout | title | guide |
---|---|---|
getting_started |
16 プロトコル - Protocols |
16 |
プロトコルは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ではfalse
とnil
だけが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"
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.
全ての型のためのデフォルトの実装が用意されていれば便利ですよね.プロトコル定義の中で@fallback_to_any
をtrue
に設定するとできるようになります:
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.
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.