Skip to content

Commit 11c7f1b

Browse files
committed
update protocol
1 parent 9f8e7ba commit 11c7f1b

File tree

1 file changed

+114
-44
lines changed

1 file changed

+114
-44
lines changed

16-proto.md

Lines changed: 114 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
16-协议
1+
16-协议(protocols)
22
========
3-
[协议和结构体](#161-%E5%8D%8F%E8%AE%AE%E5%92%8C%E7%BB%93%E6%9E%84%E4%BD%93)
4-
[回归一般化](#)
5-
[内建协议](#163-%E5%86%85%E5%BB%BA%E5%8D%8F%E8%AE%AE)
63

7-
协议是实现Elixir多态性的重要机制。任何数据类型只要实现了某协议,那么该协议的分发就是可用的。
8-
让我们看个例子。
4+
协议是实现Elixir多态性的重要机制。任何数据类型只要实现了某协议,
5+
那么基于该协议的(函数调用)消息分发就是可用的。
6+
7+
>先简单解释一下上面“分发(dispatching)”的意思:对于许多编程语言,
8+
特别是支持“duck-typing”的语言来说,对象调用方法,相当于以该对象为目的,
9+
对其发送消息(函数/方法名),希望它支持该方法调用。
10+
这里的“协议”二字对于熟悉ruby等具有“duck-typing”特性的语言的人来说会比较容易理解。
911

10-
>这里的“协议”二字对于熟悉ruby等具有duck-typing特性的语言的人来说会比较容易理解
12+
让我们看个例子
1113

12-
在Elixir中,只有false和nil被认为是false的。其它的值都被认为是true
13-
根据程序需要,有时需要一个```blank?```协议(注意,我们此处称之为“协议”),
14+
在Elixir中,只有`false``nil`被认为是“false”的。其它的值都被认为是“true”
15+
根据程序需要,有时需要一个`blank?`协议(注意,我们此处称之为“协议”),
1416
返回一个布尔值,以说明该参数是否为空。
1517
举例来说,一个空列表或者空二进制可以被认为是空的。
1618

@@ -22,9 +24,9 @@ defprotocol Blank do
2224
end
2325
```
2426

25-
从上面代码的语法上看,这个协议```Blank```声明了一个函数```blank?```,接受一个参数。
26-
看起来这个“协议”像是一份声明,需要后续的实现。
27-
下面我们为不同的数据类型实现这个协议:
27+
从上面代码的语法上看,这个协议`Blank`声明了一个函数`blank?`,接受一个参数。
28+
我们可以为不同的数据类型实现这个协议:
29+
2830
```elixir
2931
# 整型永远不为空
3032
defimpl Blank, for: Integer do
@@ -45,7 +47,7 @@ defimpl Blank, for: Map do
4547
def blank?(map), do: map_size(map) == 0
4648
end
4749

48-
# 只有false和nil这两个原子被认为是空得
50+
# 只有false和nil这两个原子被认为是“空”
4951
defimpl Blank, for: Atom do
5052
def blank?(false), do: true
5153
def blank?(nil), do: true
@@ -55,7 +57,7 @@ end
5557

5658
我们可以为所有内建数据类型实现协议:
5759
- 原子
58-
- BitString
60+
- 比特串
5961
- 浮点型
6062
- 函数
6163
- 整型
@@ -64,9 +66,10 @@ end
6466
- PID
6567
- Port
6668
- 引用
67-
- 元祖
69+
- 元组
70+
71+
现在手上有了一个协议的定义以及其实现,可如此使用之:
6872

69-
现在手边有了一个定义并被实现的协议,如此使用之:
7073
```elixir
7174
iex> Blank.blank?(0)
7275
false
@@ -77,67 +80,105 @@ false
7780
```
7881

7982
给它传递一个并没有实现该协议的数据类型,会导致报错:
83+
8084
```elixir
8185
iex> Blank.blank?("hello")
8286
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"
8387
```
8488

85-
## 16.1-协议和结构体
86-
协议和结构体一起使用能够加强Elixir的可扩展性。
89+
## 协议和结构体
90+
91+
Elixir的可扩展性(extensiblility)来源于将协议和结构体一同使用。
92+
93+
在前面几章中我们知道,尽管结构体本质上就是图(map),但是它和图并不共享各自协议的实现。
94+
像那章一样,我们先定义一个名为`User`的结构体:
8795

88-
在前面几章中我们知道,尽管结构体本质上就是图(map),但是它们和图并不共享各自协议的实现。
89-
像前几章一样,我们先定义一个名为```User```的结构体:
9096
```elixir
9197
iex> defmodule User do
9298
...> defstruct name: "john", age: 27
9399
...> end
94100
{:module, User, <<70, 79, 82, ...>>, {:__struct__, 0}}
95101
```
102+
96103
然后看看能不能用刚才定义的协议:
104+
97105
```elixir
98106
iex> Blank.blank?(%{})
99107
true
100108
iex> Blank.blank?(%User{})
101109
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}
102110
```
103111

104-
果然,结构体没有使用协议针对图的实现。
105-
因此,结构体需要使用它自己的协议实现:
112+
果然,结构体没有使用这个协议针对图的实现。
113+
因此,对于这个结构体,需要定义它的协议实现:
114+
106115
```elixir
107116
defimpl Blank, for: User do
108117
def blank?(_), do: false
109118
end
110119
```
111120

112121
如果愿意,你可以定义你自己的语法来检查一个user是否为空。
113-
不光如此,你还可以使用结构体创建更强健的数据类型(比如队列),然后实现所有相关的协议
114-
(就像枚举```Enumerable```那样),检查是否为空等等。
122+
不光如此,你还可以使用结构体创建更强健的数据类型(比如队列),然后实现所有相关的协议,
123+
比如`Enumerable`,或者是`Blank`等等。
124+
125+
## 实现`Any`
126+
127+
手动给所有类型实现某些协议实现很快就会变得犹如重复性劳动般枯燥无味。
128+
在这种情况下,Elixir给出两种选择:一是显式让我们的类型继承某些已有的实现;
129+
二是,自动给所有类型提供实现。这两种情况,我们都需要为`Any`类型写实现代码。
130+
131+
### 继承
132+
133+
Elixir允许我们继承某些有基于`Any`类型的实现。比如,我们先为`Any`实现某个协议:
134+
135+
```Elixir
136+
defimpl Blank, for: Any do
137+
def blank?(_), do: false
138+
end
139+
```
140+
141+
OK我们现在有个协议通用的实现了。在定义结构体时,可以显式标注其继承了`Blank`协议的实现:
142+
143+
```Elixir
144+
defmodule DeriveUser do
145+
@derive Blank
146+
defstruct name: "john", age: 27
147+
end
148+
```
149+
150+
继承的时候,Elixir会为`DeriveUser`实现`Blank`协议(基于`Blank``Any`上的实现)。
151+
这种方式是可选择的:结构体只会跟它们自己显式实现或继承实现的协议一起工作。
115152

116-
有些时候,程序员们希望给结构体提供某些默认的协议实现,因为显式给所有结构体都实现某些协议实在是太枯燥了。
117-
这引出了下一节“回归一般化”(falling back to any)的说法。
153+
### 退化至`Any`
154+
155+
`@derive`注解的一个替代物是显式地告诉协议,如果没有找到(在某个类型上得)实现的时候,
156+
使用其`Any`的实现(如果有)。在定义协议的时候设置`@fallback_to_any``true`即可:
118157

119-
## 16.2-回归一般化
120-
能够给所有类型提供默认的协议实现肯定是很方便的。
121-
在定义协议时,把```@fallback_to_any```设置为```true```即可:
122158
```elixir
123159
defprotocol Blank do
124160
@fallback_to_any true
125161
def blank?(data)
126162
end
127163
```
128-
现在这个协议可以被这么实现:
164+
165+
假使我们在前一小节已经完成了对`Any`的实现:
166+
129167
```elixir
130168
defimpl Blank, for: Any do
131169
def blank?(_), do: false
132170
end
133171
```
134172

135-
现在,那些我们还没有实现```Blank```协议的数据类型(包括结构体)也可以来判断是否为空了
136-
(虽然默认会被认为是false,哈哈)。
173+
现在,所有没有实现`blank`协议的数据类型(包括结构体)都会被认为是非空的。
174+
对比`@derive`,退化至`Any`是必然的:只有没有显式提供某个实现的实现,所有类型对于某个协议,都会有默认的行为。
175+
这项技术提供了很大的灵活性,也支持了Elixir程序员“显式先于隐式”的编码哲学。
176+
你可以在很多库中看到`@derive`的身影。
137177

138-
## 16.3-内建协议
139-
Elixir自带了一些内建的协议。在前面几章中我们讨论过枚举模块,它提供了许多方法。
140-
只要任何一种数据结构它实现了Enumerable协议,就能使用这些方法:
178+
## 内建协议
179+
180+
Elixir内建了一些协议。在前面几章中我们讨论过`Enum`模块,它提供了许多方法。
181+
只要任何一种数据结构它实现了`Enumerable`协议,就能使用这些方法:
141182

142183
```elixir
143184
iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end
@@ -146,34 +187,40 @@ iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end
146187
6
147188
```
148189

149-
另一个例子是```String.Chars```协议,它规定了如何将包含字符的数据结构转换为字符串类型。
190+
另一个例子是`String.Chars`协议,它规定了如何将包含字符的数据结构转换为字符串类型。
150191
它暴露为函数```to_string```
192+
151193
```elixir
152194
iex> to_string :hello
153195
"hello"
154196
```
155197

156-
注意,在Elixir中,字符串插值操作里面调用了```to_string```函数:
198+
注意,在Elixir中,字符串插值操作背后就调用了```to_string```函数:
199+
157200
```elixir
158201
iex> "age: #{25}"
159202
"age: 25"
160203
```
161-
上面代码能工作,是因为25是数字类型,而数字类型实现了```String.Chars```协议。
162-
如果传进去的是元组就会报错:
204+
205+
上面代码能工作,是因为25这个数字类型实现了`String.Chars`协议。
206+
而如果传进去的是元组就会报错:
207+
163208
```elixir
164209
iex> tuple = {1, 2, 3}
165210
{1, 2, 3}
166211
iex> "tuple: #{tuple}"
167212
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}
168213
```
169214

170-
当想要打印一个比较复杂的数据结构时,可以使用```inspect```函数。该函数基于协议```Inspect```
215+
当想要打印一个比较复杂的数据结构时,可以使用`inspect`函数。该函数基于协议`Inspect`
216+
171217
```elixir
172218
iex> "tuple: #{inspect tuple}"
173219
"tuple: {1, 2, 3}"
174220
```
175221

176-
_Inspect_ 协议用来将任意数据类型转换为可读的文字表述。IEx用来打印表达式结果用的就是它:
222+
`Inspect`协议用来将任意数据类型转换为可读的文字表述。IEx用来打印表达式结果用的就是它:
223+
177224
```elixir
178225
iex> {1, 2, 3}
179226
{1,2,3}
@@ -184,11 +231,34 @@ iex> %User{}
184231
>```inspect```是ruby中非常常用的方法。
185232
这也能看出Elixir的作者们真是绞尽脑汁把Elixir的语法尽量往ruby上靠。
186233

187-
记住,头顶着#号被插的值,会被```to_string```表现成纯字符串。
188-
在转换为可读的字符串时丢失了信息,因此别指望还能从该字符串取回原来的那个对象:
234+
记住,被执行`inspect`函数后的结果,是头顶着`#`符号的Elixir的类型描述文本,本身并不是合法的Elixir语法。
235+
在转换为可读的文本后,数值丢失了信息,因此别指望还能从该字符串取回原来的那个东西:
236+
189237
```elixir
190238
iex> inspect &(&1+2)
191239
"#Function<6.71889879/1 in :erl_eval.expr/5>"
192240
```
193241

194-
Elixir中还有些其它协议,但本章就讲这几个比较常用的。下一章将讲讲Elixir中的错误捕捉以及异常。
242+
Elixir中还有些其它协议,但本章就讲这几个比较常用的。
243+
244+
## 协议压实(consolidation)
245+
246+
当使用Mix构建工具的时候,你可能会看到如下输出:
247+
248+
```
249+
Consolidated String.Chars
250+
Consolidated Collectable
251+
Consolidated List.Chars
252+
Consolidated IEx.Info
253+
Consolidated Enumerable
254+
Consolidated Inspect
255+
```
256+
257+
这些都是Elixir内建的协议,它们正在被“压实”(压紧、夯实;还有更好的翻译么?)。
258+
因为协议可以被分发给所有的数据类型,在每一次调用时,协议必须检查是否对某数据类型提供了实现。
259+
这消耗大量资源。
260+
261+
但是,如果使用构建工具Mix,我们会知道所有的模块已被定义,包括协议和实现。
262+
这样,协议可以被优化到一个简单的易于快速分发的模块中。
263+
264+
从Elixir v1.2开始,这种优化是自动进行的。后面的《Mix和OTP教程》中会讲到。

0 commit comments

Comments
 (0)