DEV Community

gumi TECH for gumi TECH Blog

Posted on • Updated on

Elixir入門 16: プロトコル

本稿は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
Enter fullscreen mode Exit fullscreen mode

プロトコルの実装は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
Enter fullscreen mode Exit fullscreen mode

-Tuple.to_list/1: タプルをリストにします。
-Enum.map/2: 列挙可能の項目を関数に渡し、戻り値のリストを返します。
-Enum.join/2: 列挙可能の項目を文字列にして、第2引数の文字列でつなぎます。

iex> to_string({3.14, "apple", :pie})
"{3.14, apple, pie}"
Enter fullscreen mode Exit fullscreen mode

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

  • length: 計算して情報を得ます。
    • たとえば、length/1はリストの項目をすべて数えて返します。
  • size: データ構造の中にすでに計算された情報が含まれています。
    • たとえば、tuple_size/1byte_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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

プロトコルは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
Enter fullscreen mode Exit fullscreen mode

MapSet.size/1も予め計算されたサイズを返します。SizeプロトコルをMapSetに実装しましょう。

defimpl Size, for: MapSet do
  def size(set), do: MapSet.size(set)
end
Enter fullscreen mode Exit fullscreen mode
iex> Size.size(%MapSet{})
0
Enter fullscreen mode Exit fullscreen mode

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

defmodule User do
  defstruct [:name, :age]
end
defimpl Size, for: User do
  def size(_user), do: 2
end
Enter fullscreen mode Exit fullscreen mode
iex> john = %User{age: 27, name: "John"}
%User{age: 27, name: "John"}
iex> Size.size(john)
2
Enter fullscreen mode Exit fullscreen mode

anyの実装

すべてのプロトコルをひとつひとつ実装するのは、手間がかかるでしょう。Elixirでこのようときには、ふたつやり方があります。第1に、その型のプロトコル実装を派生(derive)させることです。第2は、すべての型に自動的にプロトコルを実装することもできます。いずれの場合も、Anyにプロトコルを実装しなければなりません。

派生させる

Elixirでは、Anyの実装にもとづいて、プロトコルの実装を派生させることができます。

defimpl Size, for: Any do
  def size(_), do: 0
end
Enter fullscreen mode Exit fullscreen mode

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

defmodule OtherUser do
  @derive [Size]
  defstruct [:name, :age]
end
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
iex> Size.size([1, 2, 3])
0
Enter fullscreen mode Exit fullscreen mode

ほとんどのプロトコルでは、実装されていないときエラーを返すのが適切とされています。そういう場合には、@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
Enter fullscreen mode Exit fullscreen mode

もうひとつ利用しやすい例はString.Charsプルトコルです。文字を含むデータ構造が文字列に変換できます。to_string/1関数として公開されています。

iex> to_string(:hello)
"hello"
Enter fullscreen mode Exit fullscreen mode

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

iex> age = 25
25
iex> "age: #{age}"
"age: 25"
Enter fullscreen mode Exit fullscreen mode

けれど、タプルは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
Enter fullscreen mode Exit fullscreen mode

複雑なデータ構造を出力したいときのために、Inspectプロトコルに備わるのがinspect/2関数です。

iex> "tuple: #{inspect(tuple)}"
"tuple: {1, 2, 3}"
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

プロトコルの統合

ElixirプロジェクトでビルドツールMixを使って作業していると、つぎのような出力をみることがあります。

Consolidated String.Chars
Consolidated Collectable
Consolidated List.Chars
Consolidated IEx.Info
Consolidated Enumerable
Consolidated Inspect
Enter fullscreen mode Exit fullscreen mode

これらはElixirとともに提供されているすべてのプロトコルです。それらが統合されています。プロトコルは任意のデータ型に対して実行できるので、呼び出しのたびにその型が実装しているかどうか確かめなければなりません。すると、負荷は高まります。

けれど、Mixのようなツールを使ってコンパイルすれば、定義されたすべてのモジュールは、プロトコルやその実装も含めて明らかになります。こうして、プロトコルは統合され、むだなく実行の速いモジュールになるのです。

Elixir 1.2から、すべてのプロジェクトでプロトコルの統合が自動的に行われます。プロジェクトのビルドについては、Elixir official Mix & OTP Guideをお読みください。

Elixir入門もくじ

番外

Top comments (0)