16-協議
========
[協議和結構體](#161-%E5%8D%8F%E8%AE%AE%E5%92%8C%E7%BB%93%E6%9E%84%E4%BD%93)
[回歸一般化](#)
[內建協議](#163-%E5%86%85%E5%BB%BA%E5%8D%8F%E8%AE%AE)
協議是實現Elixir多態性的重要機制。任何數據類型只要實現了某協議,那么該協議的分發就是可用的。
讓我們看個例子。
>這里的“協議”二字對于熟悉ruby等具有duck-typing特性的語言的人來說會比較容易理解。
在Elixir中,只有false和nil被認為是false的。其它的值都被認為是true。
根據程序需要,有時需要一個```blank?```協議(注意,我們此處稱之為“協議”),
返回一個布爾值,以說明該參數是否為空。
舉例來說,一個空列表或者空二進制可以被認為是空的。
我們可以如下定義協議:
```elixir
defprotocol Blank do
@doc "Returns true if data is considered blank/empty"
def blank?(data)
end
```
從上面代碼的語法上看,這個協議```Blank```聲明了一個函數```blank?```,接受一個參數。
看起來這個“協議”像是一份聲明,需要后續的實現。
下面我們為不同的數據類型實現這個協議:
```elixir
# 整型永遠不為空
defimpl Blank, for: Integer do
def blank?(_), do: false
end
# 只有空列表是“空”的
defimpl Blank, for: List do
def blank?([]), do: true
def blank?(_), do: false
end
# 只有空map是“空”
defimpl Blank, for: Map do
# 一定要記住,我們不能匹配 %{} ,因為它能match所有的map。
# 但是我們能檢查它的size是不是0
# 檢查size是很快速的操作
def blank?(map), do: map_size(map) == 0
end
# 只有false和nil這兩個原子被認為是空得
defimpl Blank, for: Atom do
def blank?(false), do: true
def blank?(nil), do: true
def blank?(_), do: false
end
```
我們可以為所有內建數據類型實現協議:
- 原子
- BitString
- 浮點型
- 函數
- 整型
- 列表
- 圖
- PID
- Port
- 引用
- 元祖
現在手邊有了一個定義并被實現的協議,如此使用之:
```elixir
iex> Blank.blank?(0)
false
iex> Blank.blank?([])
true
iex> Blank.blank?([1, 2, 3])
false
```
給它傳遞一個并沒有實現該協議的數據類型,會導致報錯:
```elixir
iex> Blank.blank?("hello")
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"
```
## 16.1-協議和結構體
協議和結構體一起使用能夠加強Elixir的可擴展性。
在前面幾章中我們知道,盡管結構體本質上就是圖(map),但是它們和圖并不共享各自協議的實現。
像前幾章一樣,我們先定義一個名為```User```的結構體:
```elixir
iex> defmodule User do
...> defstruct name: "john", age: 27
...> end
{:module, User, <<70, 79, 82, ...>>, {:__struct__, 0}}
```
然后看看能不能用剛才定義的協議:
```elixir
iex> Blank.blank?(%{})
true
iex> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}
```
果然,結構體沒有使用協議針對圖的實現。
因此,結構體需要使用它自己的協議實現:
```elixir
defimpl Blank, for: User do
def blank?(_), do: false
end
```
如果愿意,你可以定義你自己的語法來檢查一個user是否為空。
不光如此,你還可以使用結構體創建更強健的數據類型(比如隊列),然后實現所有相關的協議
(就像枚舉```Enumerable```那樣),檢查是否為空等等。
有些時候,程序員們希望給結構體提供某些默認的協議實現,因為顯式給所有結構體都實現某些協議實在是太枯燥了。
這引出了下一節“回歸一般化”(falling back to any)的說法。
## 16.2-回歸一般化
能夠給所有類型提供默認的協議實現肯定是很方便的。
在定義協議時,把```@fallback_to_any```設置為```true```即可:
```elixir
defprotocol Blank do
@fallback_to_any true
def blank?(data)
end
```
現在這個協議可以被這么實現:
```elixir
defimpl Blank, for: Any do
def blank?(_), do: false
end
```
現在,那些我們還沒有實現```Blank```協議的數據類型(包括結構體)也可以來判斷是否為空了
(雖然默認會被認為是false,哈哈)。
## 16.3-內建協議
Elixir自帶了一些內建的協議。在前面幾章中我們討論過枚舉模塊,它提供了許多方法。
只要任何一種數據結構它實現了Enumerable協議,就能使用這些方法:
```elixir
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```:
```elixir
iex> to_string :hello
"hello"
```
注意,在Elixir中,字符串插值操作里面調用了```to_string```函數:
```elixir
iex> "age: #{25}"
"age: 25"
```
上面代碼能工作,是因為25是數字類型,而數字類型實現了```String.Chars```協議。
如果傳進去的是元組就會報錯:
```elixir
iex> tuple = {1, 2, 3}
{1, 2, 3}
iex> "tuple: #{tuple}"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}
```
當想要打印一個比較復雜的數據結構時,可以使用```inspect```函數。該函數基于協議```Inspect```:
```elixir
iex> "tuple: #{inspect tuple}"
"tuple: {1, 2, 3}"
```
_Inspect_ 協議用來將任意數據類型轉換為可讀的文字表述。IEx用來打印表達式結果用的就是它:
```elixir
iex> {1, 2, 3}
{1,2,3}
iex> %User{}
%User{name: "john", age: 27}
```
>```inspect```是ruby中非常常用的方法。
這也能看出Elixir的作者們真是絞盡腦汁把Elixir的語法盡量往ruby上靠。
記住,頭頂著#號被插的值,會被```to_string```表現成純字符串。
在轉換為可讀的字符串時丟失了信息,因此別指望還能從該字符串取回原來的那個對象:
```elixir
iex> inspect &(&1+2)
"#Function<6.71889879/1 in :erl_eval.expr/5>"
```
Elixir中還有些其它協議,但本章就講這幾個比較常用的。下一章將講講Elixir中的錯誤捕捉以及異常。