どうも、靖宗です。
今回は前回もなんか文中に出てきてたProtocols(プロトコル?)。
Elixirの多態性のメカニズムと書かれてますが、前回の構造体の柔軟性を広げたバージョンでしょうか?
とりあえず、読み進めます。
復習にはなりますが、Elixirではデータサイズを測る文法が2つありました。
length
とsize
が該当するのですが、length
は捜索の必要があるのに対し、size
は事前に値が確定しているので捜索の必要が無く、高速です。(もちろんデータ型に左右されるのでリストにsize
のような操作は無理だと思います。)
ただし、size
はtuple_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
行けてます。
defprotocol
やdefimpl
の後に{: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_any
をtrue
に設定します。
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}
とあると、hoge
をto_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とかそういうツール使ってたら勝手になるって書いてるし気にしなくてよさそう。
なんかそんな機能あったな~程度で、ひとまず放置します。