loading...
gumi TECH Blog

Elixir入門 16: プロトコル

gumitech profile image gumi TECH Updated on ・4 min read

本稿は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/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
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入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide