16-協議

StraightDave發表於2014-10-05

16-協議

協議和結構體
迴歸大眾
內建協議

協議是實現Elixir多型性的重要機制。任何資料型別只要實現了某協議,那麼該協議的分發就是可用的。 讓我們看個例子。

在Elixir中,只有false和nil被認為是false。其它的值都被認為是true。 根據程式需要,有時需要一個blank?協議,返回一個布林值,以說明該引數是否為空。 舉例來說,一個空列表或者空二進位制可以被認為是空的。

我們可以如下定義協議:

defprotocol Blank do
  @doc "Returns true if data is considered blank/empty"
  def blank?(data)
end

這個協議期待一個函式blank?,它接受一個待實現的引數。 我們為不同的資料型別實現這個協議:

# Integers are never blank
defimpl Blank, for: Integer do
  def blank?(_), do: false
end

# Just empty list is blank
defimpl Blank, for: List do
  def blank?([]), do: true
  def blank?(_),  do: false
end

# Just empty map is blank
defimpl Blank, for: Map do
  # Keep in mind we could not pattern match on %{} because
  # it matches on all maps. We can however check if the size
  # is zero (and size is a fast operation).
  def blank?(map), do: map_size(map) == 0
end

# Just the atoms false and nil are blank
defimpl Blank, for: Atom do
  def blank?(false), do: true
  def blank?(nil),   do: true
  def blank?(_),     do: false
end

我們可以為所有內建資料型別實現協議:

  • 原子
  • BitString
  • 浮點型
  • 函式
  • 整型
  • 列表
  • PID
  • Port
  • 引用
  • 元祖

現在手邊有了一個定義並被實現的協議,如此使用之:

iex> Blank.blank?(0)
false
iex> Blank.blank?([])
true
iex> Blank.blank?([1, 2, 3])
false

給它傳遞一個並沒有實現該協議的資料型別,會導致報錯:

iex> Blank.blank?("hello")
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"

16.1-協議和結構體

協議和結構體一起使用能夠大大加強Elixir的可擴充套件性。

在前面幾章中我們知道,儘管結構體就是圖,但是它們和圖並不共享各自協議的實現。 像前幾章一樣,我們先定義一個名為User的結構體:

iex> defmodule User do
...>   defstruct name: "john", age: 27
...> end
{:module, User,
 <<70, 79, 82, ...>>, {:__struct__, 0}}

然後看看:

iex> Blank.blank?(%{})
true
iex> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}

結構體沒有使用協議針對圖的實現,而是使用它自己的協議實現:

defimpl Blank, for: User do
  def blank?(_), do: false
end

如果願意,你可以定義你自己的語法來檢查一個user為不為空。 不光如此,你還可以使用結構體建立更強健的資料型別,比如佇列,然後實現所有相關的協議,比如列舉(Enumerable)或檢查是否為空。

有些時候,程式設計師們希望給結構體提供某些預設的協議實現。因為顯式給所有結構體都實現某些協議實在是太枯燥了。 這引出了下一節“迴歸大眾”(falling back to any)的說法。

16.2-迴歸大眾

能夠給所有型別提供預設的協議實現肯定是很方便的。在定義協議時,把@fallback_to_any設定為true即可:

defprotocol Blank do
  @fallback_to_any true
  def blank?(data)
end

現在這個協議可以被這麼實現:

defimpl Blank, for: Any do
  def blank?(_), do: false
end

現在,那些我們還沒有實現Blank協議的資料型別(包括結構體)也可以來判斷是否為空了。

16.3-內建協議

Elixir自帶了一些內建協議。在前面幾章中我們討論過列舉模組,它提供了許多方法。 只要任何一種資料結構它實現了Enumerable協議,就能使用這些方法:

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

另一個例子是String.Chars協議,它規定了如何將包含字元的資料結構轉換為字串型別。 它暴露為函式to_string

iex> to_string :hello
"hello"

注意,在Elixir中,字串插值操作呼叫的是to_string函式:

iex> "age: #{25}"
"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}

當想要列印一個比較複雜的資料結構時,可以使用inspect函式。該函式基於協議Inspect

iex> "tuple: #{inspect tuple}"
"tuple: {1, 2, 3}"

Inspect協議用來將任意資料型別轉換為可讀的文字表述。IEx用來列印表示式結果用的就是它:

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

記住,習慣上來說,無論何時,頭頂#號被插的值,會被表現成一個不合語法的字串。 在轉換為可讀的字串時丟失了資訊,因此別指望還能從該字串取回原來的那個物件:

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

Elixir中還有些其它協議,但本章就講這幾個比較常用的。下一章將講講Elixir中的錯誤捕捉以及異常。