2018/5/21

Elixir 9 Protocol




inpsect 可以用 printable binary 形式回傳任何 value。但 elixir 是用什麼方式實作的? 是不是 guard clause

def inspect(value) when is_atom(value), do: ...
def inspect(value) when is_binary(value), do: ...

protocol 允許不同的資料類型用於相同的函數,不同資料型別的相同函數形態會有相同的行為。很像是 behavior,但 behavior 用在 module 裡面,protocol 可在 module 外面實作,這表示我們可以自由擴充 module 不足的功能。

defprotocol 定義 protocol,defprotocol 只定義 function,defimpl 實作要放在不同的地方。

defprotocol Inspect do
    def inspect(thing, opts)
end

增加了這兩個實作,就可以 inspect PID

defimpl Inspect, for: PID do
    def inspect(pid, _opts) do
        "#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))
    end
end

defimpl Inspect, for: Reference do
    def inspect(ref, _opts) do
        '#Ref' ++ rest = :erlang.ref_to_list(ref)
        "#Reference" <> IO.iodata_to_binary(rest)
    end
end
iex(1)> inspect self()
"#Process<0.89.0>"

可在 for: 後面使用的 Types 有

Any
Atom
BitString
Float
Function
Integer
List
Map
PID
Port
Record
Reference
Tuple

is_collection.exs

defprotocol Collection do
  @fallback_to_any true
  def is_collection?(value)
end

defimpl Collection, for: [List, Tuple, BitString, Map] do
  def is_collection?(_), do: true
end

defimpl Collection, for: Any do
  def is_collection?(_), do: false
end

Enum.each [ 1, 1.0, [1,2], {1,2}, %{}, "cat" ], fn value ->
  IO.puts "#{inspect value}:  #{Collection.is_collection?(value)}"
end
$ elixir is_collection.exs
1:  false
1.0:  false
[1, 2]:  true
{1, 2}:  true
%{}:  true
"cat":  true

Protocol and Structs

Elixir 沒有 classes,但支援 user-defined types

defmodule Blob do
  defstruct content: nil
end
iex(1)> c "basic.exs"
[Blob]
iex(2)> b = %Blob{content: 123}
%Blob{content: 123}
iex(3)> inspect b
"%Blob{content: 123}"

## structs 其實是 map,key為 __struct__
iex(4)> inspect b, structs: false
"%{__struct__: Blob, content: 123}"

Built-In Protocols

elixir 有以下內建的 protocols

  • Enumerable and Collectable
  • Inspect
  • List.Chars
  • String.Chars

首先定義一個以 0s 1s 表示的 integer

bitmap.exs

defmodule Bitmap do
  defstruct value: 0

  @doc """
  A simple accessor for the 2^bit value in an integer

      iex> b = %Bitmap{value: 5}
      %Bitmap{value: 5}
      iex> Bitmap.fetch_bit(b,2)
      1
      iex> Bitmap.fetch_bit(b,1)
      0
      iex> Bitmap.fetch_bit(b,0)
      1
  """
  def fetch_bit(%Bitmap{value: value}, bit) do
    use Bitwise

    (value >>> bit) &&& 1
  end
end
  • Enumerable and Collectable

Enumerable 定義了三個 functions

defprotocol Enumerable do
    # collection 的元素數量
    def count(collection)

    # 是否包含某個 value
    def member?(collection, value)

    # reduce fun to collection elements
    def reduce(collection, acc, fun)
end

針對剛剛的 Bitmap 實作 Enumerable

defimpl Enumerable,  for: Bitmap do
  import :math, only: [log: 1]

  def reduce(bitmap, {:cont, acc}, fun) do
    bit_count =  Enum.count(bitmap)
    _reduce({bitmap, bit_count}, { :cont, acc }, fun)
  end

  defp _reduce({_bitmap, -1}, { :cont, acc }, _fun), do: { :done, acc }

  defp _reduce({bitmap, bit_number}, { :cont, acc }, fun) do
    with bit = Bitmap.fetch_bit(bitmap, bit_number),
    do:  _reduce({bitmap, bit_number-1}, fun.(bit, acc), fun)
  end

  defp _reduce({_bitmap, _bit_number}, { :halt, acc }, _fun), do: { :halted, acc }

  defp _reduce({bitmap, bit_number}, { :suspend, acc }, fun), 
  do: { :suspended, acc, &_reduce({bitmap, bit_number}, &1, fun), fun } 

  def member?(value, bit_number) do
    { :ok, 0 <= bit_number && bit_number < Enum.count(value) }
  end

  def count(%Bitmap{value: value}) do              
    { :ok, trunc(log(abs(value))/log(2)) + 1 }
  end
end


fifty = %Bitmap{value: 50}

IO.puts Enum.count fifty    # => 6

IO.puts Enum.member? fifty, 4    # => true
IO.puts Enum.member? fifty, 6    # => false

IO.inspect Enum.reverse fifty       # => [0, 1, 0, 0, 1, 1, 0]
IO.inspect Enum.join fifty, ":"     # => "0:1:1:0:0:1:0"
iex(1)> c ("bitmap.exs")
[Bitmap]
iex(2)> c ("bitmap_enumerable.exs")
6
true
false
[0, 1, 0, 0, 1, 1, 0]
"0:1:1:0:0:1:0"
[Enumerable.Bitmap]

defimpl Collectable,  for: Bitmap do
  use Bitwise

  # 回傳 tuple,(1) value (2) function
  def into(%Bitmap{value: target}) do
    {target, fn
      acc, {:cont, next_bit} -> (acc <<< 1) ||| next_bit
      acc,  :done            -> %Bitmap{value: acc}
      _, :halt               -> :ok
    end}
  end
end
iex(3)> c("bitmap_collectable.exs")
[Collectable.Bitmap]
iex(4)> Enum.into [1,1,0,0,1,0], %Bitmap{value: 0}
%Bitmap{value: 50}
  • Inspect
defmodule Bitmap do
  defstruct value: 0

  defimpl Inspect do
    def inspect(%Bitmap{value: value}, _opts) do
      "%Bitmap{#{value}=#{as_binary(value)}}"
    end
    defp as_binary(value) do
      to_string(:io_lib.format("~.2B", [value]))
    end
  end
end
iex(6)> c("bitmap_inspect.exs")
[Bitmap, Inspect.Bitmap]
iex(7)> fifty = %Bitmap{value: 50}
%Bitmap{50=110010}
iex(8)> inspect fifty
"%Bitmap{50=110010}"
iex(9)> inspect fifty, structs: false
"%{__struct__: Bitmap, value: 50}"
iex(10)> %Bitmap{value: 12345678901234567890}
%Bitmap{12345678901234567890=1010101101010100101010011000110011101011000111110000101011010010}

defmodule Bitmap do
  defstruct value: 0

  defimpl Inspect, for: Bitmap do
    import Inspect.Algebra
    def inspect(%Bitmap{value: value}, _opts) do
      concat([
        nest(
         concat([
           "%Bitmap{",
           break(""),
           nest(concat([to_string(value),
                        "=",
                        break(""),
                        as_binary(value)]),
                2),
         ]), 2),
        break(""),
        "}"])
    end
    defp as_binary(value) do
      to_string(:io_lib.format("~.2B", [value]))
    end
  end
end
iex(1)> c("bitmap_algebra.exs")
[Bitmap, Inspect.Bitmap]
iex(2)>
nil
iex(3)> big_bitmap = %Bitmap{value: 12345678901234567890}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
iex(4)>
nil
iex(5)> IO.inspect big_bitmap
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
iex(6)> IO.inspect big_bitmap, structs: false
%{__struct__: Bitmap, value: 12345678901234567890}
%Bitmap{12345678901234567890=
    1010101101010100101010011000110011101011000111110000101011010010}
  • List.Chars & String.Chars

bitmap_string.exs

defimpl String.Chars, for: Bitmap do
  def to_string(bitmap) do
    import Enum
    bitmap
    |> reverse
    |> chunk(3)
    |> map(fn three_bits -> three_bits |> reverse |> join end)
    |> reverse
    |> join("_")
  end
end
iex(1)> c("bitmap.exs")
[Bitmap]
iex(2)> c("bitmap_enumerable.exs")
[Enumerable.Bitmap]
iex(3)> c("bitmap_string.exs")
[String.Chars.Bitmap]


iex(4)> fifty = %Bitmap{value: 50}
%Bitmap{value: 50}
iex(5)> "Fifty in bits is: #{fifty}"
"Fifty in bits is: 110_010"

References

Programming Elixir