技術メモ

プログラミングとか電子工作とか

Elixir入門(第十六章 Protocols)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
今回は前回もなんか文中に出てきてたProtocols(プロトコル?)。
Elixirの多態性のメカニズムと書かれてますが、前回の構造体の柔軟性を広げたバージョンでしょうか?
とりあえず、読み進めます。

復習にはなりますが、Elixirではデータサイズを測る文法が2つありました。
lengthsizeが該当するのですが、lengthは捜索の必要があるのに対し、sizeは事前に値が確定しているので捜索の必要が無く、高速です。(もちろんデータ型に左右されるのでリストにsizeのような操作は無理だと思います。)
ただし、sizetuple_size/1など型によって関数が違うので、全部行ける奴つくろうぜ!って所にプロトコルが出てくる様です。
プロトコルの定義はdefprotocolで行うようです。

defprotocol Size do
  @doc "Calculates the size (and not the length!) of a data structure"
  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で試してみます。

iex(12)> defprotocol Size do
...(12)>   @doc "Calculates the size (and not the length!) of a data structure"
...(12)>   def size(data)
...(12)> end
{:module, Size,
 <<70, 79, 82, 49, 0, 0, 19, 60, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 2, 103,
   0, 0, 0, 50, 11, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 7, 99, ...>>, {:__protocol__, 1}}
iex(13)> defimpl Size, for: BitString do
...(13)>   def size(string), do: byte_size(string)
...(13)> end
{:module, Size.BitString,
 <<70, 79, 82, 49, 0, 0, 6, 40, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 201,
   0, 0, 0, 20, 21, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 46, 66,
   105, 116, 83, 116, 114, 105, 110, 103, 8, ...>>, {:__impl__, 1}}
iex(14)> defimpl Size, for: Map do
...(14)>   def size(map), do: map_size(map)
...(14)> end
{:module, Size.Map,
 <<70, 79, 82, 49, 0, 0, 6, 12, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 188,
   0, 0, 0, 20, 15, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 46, 77,
   97, 112, 8, 95, 95, 105, 110, 102, 111, ...>>, {:__impl__, 1}}
iex(15)> defimpl Size, for: Tuple do
...(15)>   def size(tuple), do: tuple_size(tuple)
...(15)> end
{:module, Size.Tuple,
 <<70, 79, 82, 49, 0, 0, 6, 24, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 194,
   0, 0, 0, 20, 17, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 46, 84,
   117, 112, 108, 101, 8, 95, 95, 105, 110, ...>>, {:__impl__, 1}}
iex(16)> Size.size("foo")
3
iex(17)> Size.size({:ok, "hello"})
2
iex(18)> Size.size(%{label: "some label"})
1

行けてます。
defprotocoldefimplの後に{:module, ...}と出てきてるので、プロトコルも本質的にはモジュールなのでしょうか。

機能として追記していない引数とかが来ると怒られます。

iex> Size.size([1, 2, 3])
** (Protocol.UndefinedError) protocol Size not implemented for [1, 2, 3]

プロトコルの型定義の所(for:の直後)には全ての型が使える様です。

Protocols and structs(プロトコルと構造体)

Elixirの拡張性はプロトコルと構造体を共に使用したときに発揮される!!!と書かれています。
なにはともあれサンプルを読み進めます。

iex> Size.size(%{})
0
iex> set = %MapSet{} = MapSet.new
#MapSet<[]>
iex> Size.size(set)
** (Protocol.UndefinedError) protocol Size not implemented for #MapSet<[]>

('ω')。o(MapSetて何????????????)
よく分からないけどとりあえず構造体!構造体の一種の例としてだしただけだと勝手に解釈してみます。
今までに実装していない型が来たのでエラーがでていると。
なのでやることはMapSetが来たときの実装。

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

まあココまでは自然というか今までと同じ流れな気がします。

関数の実装が各々の構造体で書き分けれるので下記のような書き方も可能。

defmodule User do
  defstruct [:name, :age]
end

defimpl Size, for: User do
  def size(_user), do: 2
end

なるほど!モジュールを自分の作った構造体に合わせて拡張していける機能なんですね。

Implementing Any

TypeScriptでいうAny型みたいな処理でしょうか。
いちいち全部処理書くのはだるいから型に合う奴だけ書いてそれ以外の処理はコレ!みたいな書き方と予想しながら進んでいきます。

Deriving

とりあえずAnyという風に定義するには下記の通りにするようです。

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

ただし、こう書いたからといって別の型が全てAnyに突っ込まれる訳では無さそうです。

iex(42)> defimpl Size, for: Any do
...(42)>   def size(_), do: 0
...(42)> end
{:module, Size.Any,
 <<70, 79, 82, 49, 0, 0, 5, 228, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 179,
   0, 0, 0, 19, 15, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 46, 65,
   110, 121, 8, 95, 95, 105, 110, 102, 111, ...>>, {:__impl__, 1}}
iex(43)> Size.size("foo")
3
iex(44)> Size.size(12)
** (Protocol.UndefinedError) protocol Size not implemented for 12
    iex:37: Size.impl_for!/1
    iex:39: Size.size/1

Integer型でSize.size/1を行うと怒られます。
Anyになるかどうかは明示する必要があるようです。

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

実際にやってみます。

iex(44)> defmodule OtherUser do
...(44)>   @derive [Size]
...(44)>   defstruct [:name, :age]
...(44)> end
{:module, OtherUser,
 <<70, 79, 82, 49, 0, 0, 5, 244, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 186,
   0, 0, 0, 18, 16, 69, 108, 105, 120, 105, 114, 46, 79, 116, 104, 101, 114, 85,
   115, 101, 114, 8, 95, 95, 105, 110, 102, ...>>,
 %OtherUser{age: nil, name: nil}}
iex(45)> other = %OtherUser{}
%OtherUser{age: nil, name: nil}
iex(46)> Size.size(other)
0

なりました。
明示する必要があるなら実装すれば?という気もしますが構造体がめっちゃ増えてきた時など考えると有用な機能と言ったところでしょうか。

Fallback to Any

先ほど明示的にAnyへのderive属性を書かないとAnyの処理へ落ちないと分かりましたが、一応実装にない型はなんでもかんでもAnyへ落とす機能もあるそうです。
プロトコルを宣言するときに、属性として@fallback_to_anytrueに設定します。

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

実際になるか確認。

iex(1)> defprotocol Size do
...(1)>   @fallback_to_any true
...(1)>   def size(data)
...(1)> end
{:module, Size,
 <<70, 79, 82, 49, 0, 0, 18, 72, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 2, 68,
   0, 0, 0, 47, 11, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 7, 99, ...>>, {:__protocol__, 1}}
iex(2)> defimpl Size, for: Any do
...(2)>   def size(_), do: 0
...(2)> end
{:module, Size.Any,
 <<70, 79, 82, 49, 0, 0, 5, 216, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 179,
   0, 0, 0, 19, 15, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 46, 65,
   110, 121, 8, 95, 95, 105, 110, 102, 111, ...>>, {:__impl__, 1}}
iex(3)> Size.size(12)
0
iex(4)> Size.size(:aaa)
0
iex(5)> defimpl Size, for: BitString do
...(5)>   def size(string), do: byte_size(string)
...(5)> end
{:module, Size.BitString,
 <<70, 79, 82, 49, 0, 0, 6, 40, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 201,
   0, 0, 0, 20, 21, 69, 108, 105, 120, 105, 114, 46, 83, 105, 122, 101, 46, 66,
   105, 116, 83, 116, 114, 105, 110, 103, 8, ...>>, {:__impl__, 1}}
iex(6)> Size.size("aaa")
3

きちんと実装したBitStringはAnyになってないですが、それ以外はAnyの処理がなされています。
ただし、基本的にはderive属性を書くアプローチが採用されているようなので、あまり使わない方がいい気がします。

Built-in protocols

Elixirにはデフォルトでいくつかのプロトコルが定義されているようです。Enumモジュールもプロトコルで実装されているみたいです。

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

試しにEnumerableに該当しないものを放り込んでみます。

iex(50)> Enum.map [1, 2, 3], fn(x) -> x * 2 end
[2, 4, 6]
iex(51)> Enum.map "abc", fn(x) -> x * 2 end
** (Protocol.UndefinedError) protocol Enumerable not implemented for "abc"
    (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir) lib/enum.ex:141: Enumerable.reduce/3
    (elixir) lib/enum.ex:3015: Enum.map/2

(Protocol.UndefinedError)not implementedと記載があるのでプロトコルなのでしょう。

他にもString.Charsプロトコルとして例に挙がっています。
文字列中に#{hoge}とあると、hogeto_stringして文字列に挿入しているようです。
このto_stringが型によって実装されているという訳です。

iex(52)> to_string :hello
"hello"
iex(53)> "age: #{:hello}"
"age: hello"

なので、to_stringできない輩は文字列中の#{}でも同様に怒られます。

iex(54)> tuple = {1, 2, 3}
{1, 2, 3}
iex(55)> to_string 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
iex(55)> "age: #{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

プロトコルで実装されとらんぞ!と怒られてます。
以前にPIDの表示などで利用したinspect/1を利用すれば、文字列と化すので表示できます。

iex(55)> inspect tuple
"{1, 2, 3}"
iex(56)> "age: #{inspect tuple}"
"age: {1, 2, 3}"

このinspectプロトコルのようです。
ただし、このinspectはElixirの文法に沿わない構造は#で表されるなにかで表示され、情報が失われる(デバッグ中で分からなくなる?)ので注意してね!とのこと。

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

ここの例はだいたい分かるからいいとしてもっと複雑なコードになったときに困ったりするのかな?
必要に応じてプロトコルを使い分ける必要もありそうですが、今はスルー。

Protocol consolidation

なんかプロトコルの合体とかの話?
Mixとかそういうツール使ってたら勝手になるって書いてるし気にしなくてよさそう。
なんかそんな機能あったな~程度で、ひとまず放置します。