Elixir入门教程<中>

7-键值列表-图-字典

到目前还没有讲到任何关联性数据结构,即那种可以将一个或几个值关联到一个key上。 不同语言有不同的叫法,如字典,哈希,关联数组,图,等等。

Elixir中有两种主要的关联性结构:键值列表(keyword list)和图(map)。

7.1-键值列表

在很多函数式语言中,常用二元元组的列表来表示关联性数据结构。 在Elixir中也是这样。当我们有了一个元组(不一定仅有两个元素的元组)的列表,并且每个元组的第一个元素是个原子, 那就称之为键值列表:

iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
iex> list[:a]
1

当原子key和关联的值之间没有逗号分隔时,可以把原子的冒号拿到字母的后面。这时,原子与后面的数值之间要有一个空格。

如你所见,Elixir使用比较特殊的语法来定义这样的列表,但实际上它们会映射到一个元组列表。 因为它们是简单的列表而已,所有针对列表的操作,键值列表也可以用。

比如,可以用++运算符为列表添加元素:

iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]

上面例子中重复出现了:a这个key,这是允许的。 以这个key取值时,取回来的是第一个找到的(因为有序):

iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0

键值列表十分重要,它有两大特点: - 有序 - key可以重复(!仔细看上面两个示例)

例如,Ecto库使用这两个特点写出了精巧的DSL(用来写数据库query):

query = from w in Weather,
          where: w.prcp > 0,
          where: w.temp < 20,
        select: w

这些特性使得键值列表成了Elixir中为函数提供额外选项的默认手段。 在第5章我们讨论了if/2宏,提到了下方的语法:

iex> if false, do: :this, else: :that
:that

do: 和else: 就是键值列表!事实上代码等同于:

iex> if(false, [do: :this, else: :that])
:that

当键值列表是函数最后一个参数时,方括号就成了可选的。

为了操作关键字列表,Elixir提供了键值(keyword)模块。 记住,键值列表就是简单的列表,和列表一样提供了线性的性能。列表越长,获取长度或找到一个键值的速度越慢。 因此,关键字列表在Elixir中一般就作为函数调用的可选项。 如果你要存储大量数据,并且保证一个键只对应最多一个值,那就使用图。

对键值列表做模式匹配:

iex> [a: a] = [a: 1]
[a: 1]
iex> a
1
iex> [a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
iex> [b: b, a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]

尽管如此,对列表使用模式匹配很少用到。因为不但要元素个数相等,顺序还要匹配。

7.2-图(maps)

无论何时想用键-值结构,图都应该是你的第一选择。Elixir中,用%{}定义图:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b

和键值列表对比,图有两主要区别: - 图允许任何类型值作为键 - 图的键没有顺序

如果你向图添加一个已有的键,将会覆盖之前的键-值对:

iex> %{1 => 1, 1 => 2}
%{1 => 2}

如果图中的键都是原子,那么你也可以用键值列表中的一些语法:

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}

对比键值列表,图的模式匹配很是有用:

iex> %{} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

如上所示,图A与另一个图B做匹配。图B中只要包含有图A的键,那么两个图就能匹配上。若图A是个空的,那么任意图B都能匹配上。 但是如果图B里不包含图A的键,那就匹配失败了。

图还有个有趣的功能:它提供了特殊的语法来修改和访问原子键:

iex> map = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> map.a
1
iex> %{map | :a => 2}
%{:a => 2, 2 => :b}
iex> %{map | :c => 3}
** (ArgumentError) argument error

使用上面两种语法要求的前提是所给的键是切实存在的。最后一条语句错误的原因就是键:c不存在。

未来几章中我们还将讨论结构体(structs)。结构体提供了编译时的保证,它是Elixir多态的基础。 结构体是基于图的,上面例子提到的修改键值的前提就变得十分重要。

图模块提供了许多关于图的操作。它提供了与键值列表许多相似的API,因为这两个数据结构都实现了字典的行为。

图是最近连同EEP 43被引入Erlang虚拟机的。 Erlang 17提供了EEP的部分实现,只支持_一小部分_图功能。 这意味着图仅在存储不多的键时,图的性能还行。为了解决这个问题,Elixir还提供了HashDict模块该模块提供了一个字典来支持大量的键,并且性能不错。

7.3-字典(Dicts)

Elixir中,键值列表和图都被称作字典。换句话说,一个字典就像一个接口(在Elixir中称之为行为behaviour)。 键值列表和图模块实现了该接口。

这个接口定义于Dict模块,该模块还提供了底层实现的一个API:

iex> keyword = []
[]
iex> map = %{}
%{}
iex> Dict.put(keyword, :a, 1)
[a: 1]
iex> Dict.put(map, :a, 1)
%{a: 1}

字典模块允许开发者实现他们自己的字典形式,提供一些特殊的功能,然后联系到现存的的Elixir代码中去。 字典模块还提供了所有字典类型都可以使用的函数。如,Dicr.equal?/2可以比较两个字典类型(可以是不同的实现)。

你会疑惑些程序时用keyword,Map还是Dict模块呢?答案是:看情况。

如果你的代码期望接受一个关键字作为参数,那么使用简直列表模块。如果你想操作一个图,那就使用图模块。 如果你想你的API对所有字典类型的实现都有用,那就使用字典模块(确保以不同的实现作为参数测试一下)。

8-模块

Elixir中我们把许多函数组织成一个模块。我们在前几章已经提到了许多模块,如String模块

iex> String.length "hello"
5

创建自己的模块,用defmodule宏。用def宏在其中定义函数:

iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3

像ruby一样,模块名大写起头

8.1-编译

通常把模块写进文件,这样可以编译和重用。假如文件math.ex有如下内容:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

这个文件可以用elixirc进行编译:

$ elixirc math.ex

这将生成名为Elixir.Math.beam的bytecode文件。 如果这时再启动iex,那么这个模块就已经可以用了(假如在含有该编译文件的目录启动iex):

iex> Math.sum(1, 2)
3

Elixir工程通常组织在三个文件夹里: - ebin,包括编译后的字节码 - lib,包括Elixir代码(.ex文件) - test,测试代码(.exs文件)

实际项目中,构建工具Mix会负责编译,并且设置好正确的路径。 而为了学习方便,Elixir也提供了脚本模式,可以更灵活而不用编译。

8.2-脚本模式

除了.ex文件,Elixir还支持.exs脚本文件。Elixir对两种文件一视同仁,唯一区别是.ex文件有待编译, 而.exs文件用来作脚本执行,不需要编译。例如,如下创建名为math.exs的文件:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)

执行之:

$ elixir math.exs

这中情况,文件将在内存中编译和执行,打印出“3”作为结果。没有比特码文件生成。 后文中(为了学习和练习方便),推荐使用脚本模式执行学到的代码。

8.3-命名函数

在某模块中,我们可以用def/2宏定义函数,用defp/2定义私有函数。 用def/2定义的函数可以被其它模块中的代码使用,而私有函数仅在定义它的模块内使用。

defmodule Math do
  def sum(a, b) do
    do_sum(a, b)
  end

  defp do_sum(a, b) do
    a + b
  end
end

Math.sum(1, 2)    #=> 3
Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

函数声明也支持使用卫兵或多个子句。 如果一个函数有好多子句,Elixir会匹配每一个子句直到找到一个匹配的。 下面例子检查参数是否是数字:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_number(x) do
    false
  end
end

Math.zero?(0)  #=> true
Math.zero?(1)  #=> false

Math.zero?([1,2,3])
#=> ** (FunctionClauseError)

如果没有一个子句能匹配参数,会报错。

8.4-函数捕捉

本教程中提到函数,都是用name/arity的形式描述。这种表示方法可以被用来获取一个命名函数(赋给一个函数型变量)。 下面用iex执行一下上文定义的math.exs文件:

$ iex math.exs
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function fun
true
iex> fun.(0)
true

&<function notation>从函数名捕捉一个函数,它本身代表该函数值(函数类型的值)。 它可以不必赋给一个变量,直接用括号来使用该函数。 本地定义的,或者已导入的函数,比如is_function/1,可以不前缀模块名:

iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true

这种语法还可以作为快捷方式使用与创建函数:

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

&1 表示传给该函数的第一个参数。上面例子中,&(&1+1)其实等同于n x->x+1 end。 在创建短小函数时,这个很方便。想要了解更多关于&捕捉操作符,参考Kernel.SpecialForms文档

8.5-默认参数

Elixir中,命名函数也支持默认参数:

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

任何表达式都可以作为默认参数,但是只在函数调用时用到了才被执行(函数定义时,那些表达式只是存在那儿,不执行;函数调用时,没有用到默认值,也不执行)。

defmodule DefaultTest do
  def dowork(x \\ IO.puts "hello") do
    x
  end
end
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
hello
:ok

如果有默认参数值的函数有了多条子句,推荐先定义一个函数头(无具体函数体)仅为了声明这些默认值:

defmodule Concat do
  def join(a, b \\ nil, sep \\ " ")

  def join(a, b, _sep) when nil?(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

使用默认值时,注意对函数重载会有一定影响。考虑下面例子:

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep \\ " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

如果将以上代码保存在文件“concat.ex”中并编译,Elixir会报出以下警告:

concat.ex:7: this clause cannot match because a previous clause at line 2 always matches

编译器是在警告我们,在使用两个参数调用join函数时,总使用第一个函数定义。 只有使用三个参数调用时,才会使用第二个定义:

$ iex concat.exs
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"

后面几章将介绍使用命名函数来做循环,如何从别的模块中导入函数,以及模块的属性等。

9-递归

因为在Elixir中(或所有函数式语言中),数据有不变性(immutability),因此在写循环时与传统的命令式(imperative)语言有所不同。 例如某命令式语言的循环可以这么写:

for(i = 0; i < array.length; i++) {
  array[i] = array[i] * 2
}

上面例子中,我们改变了array,以及辅助变量i的值。这在Elixir中是不可能的。 尽管如此,函数式语言却依赖于某种形式的循环—递归:一个函数可以不断地被递归调用,直到某条件满足才停止。 考虑下面的例子:打印一个字符串若干次:

defmodule Recursion do
  def print_multiple_times(msg, n) when n <= 1 do
    IO.puts msg
  end

  def print_multiple_times(msg, n) do
    IO.puts msg
    print_multiple_times(msg, n - 1)
  end
end

Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!

一个函数可以有许多子句(上面看起来定义了两个函数,但卫兵条件不同,可以看作同一个函数的两个子句)。 当参数匹配该子句的模式,且该子句的卫兵表达式返回true,才会执行该子句内容。 上面例子中,当print_multiple_times/2第一次调用时,n的值是3。

第一个子句有卫兵表达式要求n必须小于等于1。因为不满足此条件,代码找该函数的下一条子句。

参数匹配第二个子句,且该子句也没有卫兵表达式,因此得以执行。 首先打印msg,然后调用自身并传递第二个参数n-1(=2)。 这样msg又被打印一次,之后调用自身并传递参数n-1(=1)。

这个时候,n满足第一个函数子句条件。遂执行该子句,打印msg,然后就此结束。

我们称例子中第一个函数子句这种子句为“基本情形”。 基本情形总是最后被执行,因为起初通常都不匹配执行条件,程序而转去执行其它子句。 但是,每执行一次其它子句,条件都向这基本情形靠拢一点,直到最终回到基本情形处执行代码。

下面我们使用递归的威力来计算列表元素求和:

defmodule Math do
  def sum_list([head|tail], accumulator) do
    sum_list(tail, head + accumulator)
  end

  def sum_list([], accumulator) do
    accumulator
  end
end

Math.sum_list([1, 2, 3], 0) #=> 6

我们首先用列表[1,2,3]和初值0作为参数调用函数,程序将逐个匹配各子句的条件,执行第一个符合要求的子句。 于是,参数首先满足例子中定义的第一个子句。参数匹配使得head = 1,tail = [2,3],accumulator = 0。

然后执行该字据内容,把head + accumulator作为第二个参数,连带去掉了head的列表做第一个参数,再次调用函数本身。 如此循环,每次都把新传入的列表的head加到accumulator上,传递去掉了head的列表。 最终传递的列表为空,符合第二个子句的条件,执行该子句,返回accumulator的值6。

几次函数调用情况如下:

sum_list [1, 2, 3], 0
sum_list [2, 3], 1
sum_list [3], 3
sum_list [], 6

这种使用列表做参数,每次削减一点列表的递归方式,称为“递减”算法,是函数式编程的核心。
如果我们想给每个列表元素加倍呢?:

defmodule Math do
  def double_each([head|tail]) do
    [head * 2| double_each(tail)]
  end

  def double_each([]) do
    []
  end
end

Math.double_each([1, 2, 3]) #=> [2, 4, 6]

此处使用了递归来遍历列表元素,使它们加倍,然后返回新的列表。 这样以列表为参数,递归处理其每个元素的方式,称为“映射(map)”算法。

递归和列尾调用优化(tail call optimization)是Elixir中重要的部分,通常用来创建循环。 尽管如此,在Elixir中你很少会使用以上方式来递归地处理列表。

下一章要介绍的Enum模块为操作列表提供了诸多方便。 比如,下面例子:

iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end)
[2, 4, 6]

或者,使用捕捉的语法:

iex> Enum.reduce([1, 2, 3], 0, &+/2)
6
iex> Enum.map([1, 2, 3], &(&1 * 2))
[2, 4, 6]

10-枚举类型和流

10.1-枚举类型

Elixir提供了枚举类型(enumerables)的概念,使用[Enum模块]()操作它们。我们已经介绍过两种枚举类型:列表和图。

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

Enum模块为枚举类型提供了大量函数来变化,排序,分组,过滤和读取元素。 Enum模块是开发者最常用的模块之一。

Elixir还提供了范围(range):

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6

因为Enum模块在设计时为了适用于不同的数据类型,所以它的API被限制为多数据类型适用的函数。 为了实现某些操作,你可能需要针对某类型使用某特定的模块。 比如,如果你要在列表中某特定位置插入一个元素,要用List模块中的List.insert_at/3函数。而向某些类型内插入数据是没意义的,比如范围。

Enum中的函数是多态的,因为它们能处理不同的数据类型。 尤其是,模块中可以适用于不同数据类型的函数,它们是遵循了Enumerable协议。 我们在后面章节中将讨论这个协议。下面将介绍一种特殊的枚举类型:流。

10.2-积极vs懒惰

Enum模块中的所有函数都是积极的。多数函数接受一个枚举类型,并返回一个列表:

iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.filter(1..3, odd?)
[1, 3]

这意味着当使用Enum进行多种操作时,每次操作都生成一个中间列表,直到得出最终结果:

iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

上面例子是一个含有多个操作的管道。从一个范围开始,然后给每个元素乘以3。 该操作将会生成的中间结果是含有100000个元素的列表。 然后我们过滤掉所有偶数,产生又一个新中间结果:一个50000元素的列表。 最后求和,返回结果。 >这个符号的用法似乎和F#中的不一样啊…

作为一个替代,流模块提供了懒惰的实现:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

与之前Enum的处理不同,流先创建了一系列的计算操作。然后仅当我们把它传递给Enum模块,它才会被调用。流这种方式适用于处理大量的(甚至是无限的)数据集合。

10.3-流

流是懒惰的,比起Enum来说。 分步分析一下上面的例子,你会发现流与Enum的区别:

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

流操作返回的不是结果列表,而是一个数据类型—流,一个表示要对范围1..100000使用map操作的动作。

另外,当我们用管道连接多个流操作时:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>

流模块中的函数接受任何枚举类型为参数,返回一个流。 流模块还提供了创建流(甚至是无限操作的流)的函数。 例如,Stream.cycle/1可以用来创建一个流,它能无限周期性枚举所提供的参数(小心使用):

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.cycle/1>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

另一方面,Stream.unfold/2函数可以生成给定的有限值:

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]

另一个有趣的函数是Stream.resource/3,它可以用来包裹某资源,确保该资源在使用前打开,在用完后关闭(即使中途出现错误)。–类似C#中的use{}关键字。 比如,我们可以stream一个文件:

iex> stream = File.stream!("path/to/file")
#Function<18.16982430/2 in Stream.resource/3>
iex> Enum.take(stream, 10)

这个例子读取了文件的前10行内容。流在处理大文件,或者慢速资源(如网络)时非常有用。

一开始Enum和流模块中函数的数量多到让人气馁。但你会慢慢地熟悉它们。 建议先熟悉Enum模块,然后因为应用而转去流模块中那些相应的,懒惰版的函数。

11-进程

Elixir中,所有代码都在进程中执行。进程彼此独立,一个接一个并发执行,彼此通过消息传递来沟通。 进程不仅仅是Elixir中最基本的并发单位,它们还是Elixir创建分布式和高容错程序的基础。

Elixir的进程和操作系统中的进程不可混为一谈。 Elixir的进程非常非常地轻量级(在使用CPU和内存角度上说。但是它又不同于其它语言中的线程)。 因此,同时运行着数万个进程也并不是罕见的事。

本章将讲解派生新进程的基本知识,以进程间如何发送和接受消息。

11.1-进程派生

派生(spawning)一个新进程的方法是使用自动导入(kernel函数)的spawn/1函数:

iex> spawn fn -> 1 + 2 end
#PID<0.43.0>

函数spawn/1接收一个_函数_作为参数,在其派生出的进程中执行这个函数。

注意spawn/1返回一个PID(进程标识)。在这个时候,这个派生的进程很可能已经结束。 派生的进程执行完函数后便会结束:

iex> pid = spawn fn -> 1 + 2 end
#PID<0.44.0>
iex> Process.alive?(pid)
false

你可能会得到与例子中不一样的PID

self/0函数获取当前进程的PID:

iex> self()
#PID<0.41.0>
iex> Process.alive?(self())
true

而可以发送和接收消息,让进程变得越来越有趣。

11.2-发送和接收

使用send/2函数发送消息,用receive/1接收消息:

iex> send self(), {:hello, "world"}
{:hello, "world"}
iex> receive do
...>   {:hello, msg}  -> msg
...>   {:world, msg} -> "won't match"
...> end
"world"

当有消息被发给某进程,该消息就被存储在该进程的邮箱里。receive/1语句块检查当前进程的邮箱,寻找匹配给定模式的消息。 函数receive/1支持分支子句,如case/2。 当然也可以给子句加上卫兵表达式。

如果找不到匹配的消息,当前进程将一直等待,知道下一条信息到达。但是可以设置一个超时时间:

iex> receive do
...>   {:hello, msg}  -> msg
...> after
...>   1_000 -> "nothing after 1s"
...> end
"nothing after 1s"

超时时间设为0表示你知道当前邮箱内肯定有邮件存在,很自信,因此设了这个极短的超时时间。

把以上概念综合起来,演示进程间发送消息:

iex> parent = self()
#PID<0.41.0>
iex> spawn fn -> send(parent, {:hello, self()}) end
#PID<0.48.0>
iex> receive do
...>   {:hello, pid} -> "Got hello from #{inspect pid}"
...> end
"Got hello from #PID<0.48.0>"

在shell中执行程序时,辅助函数flush/0很有用。它清空缓冲区,打印进程邮箱中的所有消息:

iex> send self(), :hello
:hello
iex> flush()
:hello
:ok

11.3-链接

Elixir中最常用的进程派生方式是通过函数spawn_link/1。 在举例子讲解spawn_link/1之前,来看看如果一个进程失败了会发生什么:

iex> spawn fn -> raise "oops" end
#PID<0.58.0>

。。。啥也没发生。这时因为进程都是互不干扰的。如果我们希望一个进程中发生失败可以被另一个进程知道,我们需要链接它们。 使用spawn_link/1函数,例子:

iex> spawn_link fn -> raise "oops" end
#PID<0.60.0>
** (EXIT from #PID<0.41.0>) an exception was raised:
    ** (RuntimeError) oops
        :erlang.apply/2

当失败发生在shell中,shell会自动终止执行,并显示失败信息。这导致我们没法看清背后过程。 要弄明白链接的进程在失败时发生了什么,我们在一个脚本文件使用spawn_link/1并且执行和观察它:

# spawn.exs
spawn_link fn -> raise "oops" end

receive do
  :hello -> "let's wait until the process fails"
end

这次,该进程在失败时把它的父进程也弄停止了,因为它们是链接的。
手动链接进程:Process.link/1。 建议可以多看看Process模块,里面包含很多常用的进程操作函数。

进程和链接在创建能高容错系统时扮演重要角色。在Elixir程序中,我们经常把进程链接到某“管理者”上。 由这个角色负责检测失败进程,并且创建新进程取代之。因为进程间独立,默认情况下不共享任何东西。 而且当一个进程失败了,也不会影响其它进程。 因此这种形式(进程链接到“管理者”角色)是唯一的实现方法。

其它语言通常需要我们来try-catch异常,而在Elixir中我们对此无所谓,放手任进程挂掉。 因为我们希望“管理者”会以更合适的方式重启系统。 “要死你就快一点”是Elixir软件开发的通用哲学。

在讲下一章之前,让我们来看一个Elixir中常见的创建进程的情形。

11.4-状态

目前为止我们还没有怎么谈到状态。但是,只要你创建程序,就需要状态。 例如,保存程序的配置信息,或者分析一个文件先把它保存在内存里。 你怎么存储状态?

进程就是(最常见的)答案。我们可以写无限循环的进程,保存一个状态,然后通过收发信息来告知或改变该状态。 例如,写一个模块文件,用来创建一个提供k-v仓储服务的进程:

defmodule KV do
  def start do
    {:ok, spawn_link(fn -> loop(%{}) end)}
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end
end

注意start函数简单地派生一个新进程,这个进程以一个空的图为参数,执行loop/1函数。 这个loop/1函数等待消息,并且针对每个消息执行合适的操作。 加入受到一个:get消息,它把消息发回给调用者,然后再次调用自身loop/1,等待新消息。 当受到:put消息,它便用一个新版本的图变量(里面的k-v更新了)再次调用自身。

执行一下试试:

iex> {:ok, pid} = KV.start
#PID<0.62.0>
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
nil

一开始进程内的图变量是没有键值的,所以发送一个:get消息并且刷新当前进程的收件箱,返回nil。 下面再试试发送一个:put消息:

iex> send pid, {:put, :hello, :world}
#PID<0.62.0>
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
:world

注意进程是怎么保持一个状态的:我们通过同该进程收发消息来获取和更新这个状态。 事实上,任何进程只要知道该进程的PID,都能读取和修改状态。

还可以注册这个PID,给它一个名称。这使得人人都知道它的名字,并通过名字来向它发送消息:

iex> Process.register(pid, :kv)
true
iex> send :kv, {:get, :hello, self()}
{:get, :hello, #PID<0.41.0>}
iex> flush
:world

使用进程维护状态,以及注册进程都是Elixir程序非常常用的方式。 但是大多数时间我们不会自己实现,而是使用Elixir提供的抽象实现。 例如,Elixir提供的agent就是一种维护状态的简单的抽象实现:

iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.72.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world

Agent.start/2方法加一个一个:name选项可以自动为其注册一个名字。 除了agents,Elixir还提供了创建通用服务器(generic servers,称作GenServer)、 通用时间管理器以及事件处理器(又称GenEvent)的API。 这些,连同“管理者”树,都可以在Mix和OTP手册里找到详细说明。

12-I/O

本章简单介绍Elixir的输入、输出机制,以及相关的模块,如IO文件路径

现在介绍I/O似乎有点早,但是I/O系统可以让我们一窥Elixir哲学,满足我们对该语言以及VM的好奇心。

12.1-IO模块

IO模块是Elixir语言中读写标准输入、输出、标准错误、文件、设备的主要机制。 使用该模块的方法颇为直接:

iex> IO.puts "hello world"
"hello world"
:ok
iex> IO.gets "yes or no? "
yes or no? yes
"yes\n"

IO模块中的函数默认使用标准输入输出。 我们也可以传递:stderr来指示将错误信息写到标准错误设备上:

iex> IO.puts :stderr, "hello world"
"hello world"
:ok

12.2-文件模块

文件模块包含了可以让我们读写文件的函数。 默认情况下文件是以二进制模式打开,它要求程序员使用特殊的IO.binread/2IO.binwrite/2函数来读写文件:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}
iex> IO.binwrite file, "world"
:ok
iex> File.close file
:ok
iex> File.read "hello"
{:ok, "world"}

文件可以使用:utf8编码打开,然后就可以被IO模块中其他函数使用了:

iex> {:ok, file} = File.open "another", [:write, :utf8]
{:ok, #PID<0.48.0>}

除了打开、读写文件的函数,文件模块还有许多函数来操作文件系统。这些函数根据Unix功能相对应的命令命名。 如File.rm/1用来删除文件;File.mkdir/1用来创建目录;File.mkdir_p/1创建目录并保证其父目录一并创建; 还有File.cp_r/2File.rm_rf/2用来递归地复制和删除整个目录。

你还会注意到文件模块中,函数一般有一个名称类似的版本。区别是名称上一个有!(bang)一个没有。 例如,上面的例子我们在读取“hello”文件时,用的是不带!号的版本。 下面用例子演示下它们的区别:

iex> File.read "hello"
{:ok, "world"}
iex> File.read! "hello"
"world"
iex> File.read "unknown"
{:error, :enoent}
iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory

注意看,当文件不存在时,带!号的版本会报错。就是说不带!号的版本能照顾到模式匹配出来的不同的情况。 但有的时候,你就是希望文件在那儿,!使得它能报出有意义的错误。 因此,不要写:

{:ok, body} = File.read(file)

相反地,应该这么写:

case File.read(file) do
  {:ok, body} -> # handle ok
  {:error, r} -> # handle error
end

或者

File.read!(file)

12.3-路径模块

文件模块中绝大多数函数都以路径作为参数。通常这些路径都是二进制,可以被路径模块提供的函数操作:

iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"


有了以上介绍的几个模块和函数,我们已经能对文件系统进行基本的IO操作。 下面将讨论IO令人好奇的高级话题。这部分不是写Elixir程序必须掌握的,可以跳过不看。 但是如果你大概地看看,可以了解一下IO是如何在VM上实现以及其它一些有趣的内容。

12.4-进程和组长

你可能已经发现,File.open/2函数返回了一个包含PID的元祖:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}

这是因为IO模块实际上是同进程协同工作的。当你调用IO.write(pid, binary)时,IO模块将发送一条消息给执行操作的进程。 让我们用自己的代码表述下这个过程:

iex> pid = spawn fn ->
...>  receive do: (msg -> IO.inspect msg)
...> end
#PID<0.57.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.41.0>, #PID<0.57.0>, {:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated

调用IO.write/2之后,可以看见打印出了发给IO模块的请求。然而因为我们没有提供某些东西,这个请求失败了。

StringIO模块提供了一个基于字符串的IO实现:

iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.43.0>}
iex> IO.read(pid, 2)
"he"

Erlang虚拟机用进程给IO设备建模,允许同一个网络中的不同节点通过交换文件进程,实现节点间的文件读写。 在所有IO设备之中,有一个特殊的进程,称作组长(group leader)。

当你写东西到标准输出,实际上是发送了一条消息给组长,它把内容写给*STDIO文件表述者*:

iex> IO.puts :stdio, "hello"
hello
:ok
iex> IO.puts Process.group_leader, "hello"
hello
:ok

组长可为每个进程做相应配置,用于处理不同的情况。 例如,当在远程终端执行代码时,它能保证远程机器的消息可以被重定向到发起操作的终端上。

12.5-*iodata*和chardata

在以上所有例子中,我们都用的是二进制/字符串方式读写文件。 在“二进制、字符串和字符列表”那章里,我们注意到字符串就是普通的bytes,而字符列表是code points的列表。

IO模块和文件模块中的函数接受列表作为参数。这也就算了,其实还可以接受混合类型的列表,里面是整形、二进制都行:

iex> IO.puts 'hello world'
hello world
:ok
iex> IO.puts ['hello', ?\s, "world"]
hello world
:ok

尽管如此,有些地方还是要注意。一个列表可能表示一串byte,或者一串字符。用哪一种看IO设备是怎么编码的。 如果不指明编码,文件就以raw模式打开,这时候只能用文件模块里bin*开头(二进制版)的函数对其进行操作。 这些函数接受*iodata*作为参数,即,它们期待一个整数值的列表,用来表示byte或二进制。

尽管只是细微的差别,但你只需要考虑那些细节,如果你打算传递列表给那些函数。 底层的bytes已经可以表示二进制,这种表示就是raw的。

13-别名和代码引用

为了实现软件重用,Elixir提供了三种指令(directives)。之所以称之为“指令”是因为它们的作用域是词法作用域(lexicla scope)。

13.1-别名

alias可以为任何模块名设置别名。想象一下Math模块,它针对特殊的数学运算使用了特殊的列表实现:

defmodule Math do
  alias Math.List, as: List
end

现在,任何对List的引用将被自动变成对Math.List的引用。 如果还想访问原来的List,需要前缀’Elixir’:

List.flatten             #=> uses Math.List.flatten
Elixir.List.flatten      #=> uses List.flatten
Elixir.Math.List.flatten #=> uses Math.List.flatten

Elixir中定义的所有模块都在一个主Elixir命名空间。但是为方便起见,我们平时都不再前面加‘Elixir’。

别名常被使用与定义快捷方式中。实际上不带as选项去调用alias会自动将这个别名设置为模块名的最后一部分:

alias Math.List

就相当于:

alias Math.List, as: List

注意,别名是词法作用域。即,允许你在某个函数中设置别名:

defmodule Math do
  def plus(a, b) do
    alias Math.List
    # ...
  end

  def minus(a, b) do
    # ...
  end
end

上面例子中,alias指令只在函数plus/2中有用,minus/2不受影响。

13.2-require

Elixir提供了许多宏,用于元编程(写能生成代码的代码)。

宏就是一堆代码,只是它是在编译时被展开和执行。就是说,为了使用一个宏,你需要确保它的模块和实现在编译时可用。 这使用require指令:

iex> Integer.odd?(3)
** (CompileError) iex:1: you must require Integer before invoking the macro Integer.odd?/1
iex> require Integer
nil
iex> Integer.odd?(3)
true

Elixir中,Integer.odd?/1函数被定义为一个宏,因此他可以被当作卫兵表达式(guards)使用。 为了调用这个宏,你首先得确保用require引用了_Integer_模块。

总的来说,一个模块在被用到之前不需要被require引用,除非我们想让这个宏在整个模块中可用。 尝试调用一个没有引入的宏会导致报错。注意,像alias指令一样,require也是词法作用域的。 下文中我们会进一步讨论宏。

13.3-import

当我们想轻松地访问别的模块中的函数和宏时,我们使用import指令加上那个模块完整的名字。 例如,如果我们想多次使用List模块中的duplicate函数,我们可以简单地import它:

iex> import List, only: [duplicate: 2]
nil
iex> duplicate :ok, 3
[:ok, :ok, :ok]

这个例子中,我们只从List模块导入了函数duplicate/2。尽管only:选项是可选的,但是推荐使用。 除了only:选项,还有except:选项。 使用选项only:还可以传递给它:macros:functions。如下面例子,程序仅导入Integer模块中所有的宏:

import Integer, only: :macros

或者,仅导入所有的函数:

import Integer, only: :functions

注意,import也是词法作用域,意味着我们可以在某特定函数中导入宏或方法:

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    # call duplicate
  end
end

在此例子中,导入的函数List.duplicate/2只在那个函数中可见。这个模块中其它函数都不可用(自然,别的模块也不受影响)。

注意,若import一个模块,将自动require它。

13.4-别名机制

讲到这里你会问,Elixir的别名到底是什么,它是怎么实现的?

Elixir中的别名是以大写字母开头的标识符(像String, Keyword等等),在编译时会被转换为原子。 例如,别名‘String’会被转换为:"Elixir.String"

iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String"
String

使用alias/2指令,其实只是简单地改变了这个别名将要转换的结果。

别名如此工作,是因为在Erlang虚拟机中(以及后来的Elixir),模块是被表述成原子的。例如,我们调用一个Erlang模块的机制是:

iex> :lists.flatten([1,[2],3])
[1, 2, 3]

这也是允许我们动态调用某模块内给定函数的机制:

iex> mod = :lists
:lists
iex> mod.flatten([1,[2],3])
[1,2,3]

一句话,我们只是简单地在原子:lists上调用了函数flatten

13.5-嵌套

介绍了别名,现在可以讲讲嵌套(nesting)以及它在Elixir中是如何工作的。

考虑下面的例子:

defmodule Foo do
  defmodule Bar do
  end
end

该例子定义了两个模块_Foo_和_Foo.Bar_。后者在_Foo_中可以用_Bar_为名来访问,因为它们在同一个词法作用域中。 如果之后开发者决定把Bar模块挪到另一个文件中,那它就需要以全名(Foo.Bar)或者别名来指代。

换句话说,上面的代码等同于:

defmodule Elixir.Foo do
  defmodule Elixir.Foo.Bar do
  end
  alias Elixir.Foo.Bar, as: Bar
end

在以后章节我们可以看到,别名在宏机制中扮演了很重要的角色,来保证宏是干净的(hygienic)。

讨论到这里,模块基本上讲得差不多了。之后会讲解模块的属性。

14-模块属性

在Elixir中,模块属性(attributes)主要服务于三个目的: 1. 作为一个模块的注释,通常附加上用户或虚拟机用到的信息 2. 作为常量 3. 在编译时作为一个临时的模块存储

让我们一个一个讲解。

14.1-作为注释

Elixir从Erlang带来了模块属性的概念。例子:

defmodule MyServer do
  @vsn 2
end

这个例子中,我们显式地为该模块设置了版本这个属性。 属性标识@vsn会被Erlang虚拟机的代码装载机制用到,读取并检查该模块是否在某处被更新了。 如果不注明版本号,会设置为这个模块函数的md5 checksum。

Elixir有个好多系统保留属性。这里只列举一些最常用的: - @moduledoc 为当前模块提供文档 - @doc 为该属性后面的函数或宏提供文档 - @behaviour (注意这个单词是英式拼法)用来注明一个OTP或用户自定义行为 - @before_compile 提供一个每当模块被编译之前执行的钩子。这使得我们可以在模块被编译之前往里面注入函数。

@moduledoc和@doc是很常用的属性,推荐经常使用(写文档)。

Elixir视文档为一等公民,提供了很多方法来访问文档。

让我们回到上几章定义的Math模块,为它添加文档,然后依然保存在math.ex文件中:

defmodule Math do
  @moduledoc """
  Provides math-related functions.

  ### Examples

      iex> Math.sum(1, 2)
      3

  """

  @doc """
  Calculates the sum of two numbers.
  """
  def sum(a, b), do: a + b
end

上面例子使用了heredocs注释。heredocs是多行的文本,用三个引号包裹,保持里面内容的格式。 下面例子演示在iex中,用h命令读取模块的注释:

$ elixirc math.ex
$ iex
iex> h Math # Access the docs for the module Math
...
iex> h Math.sum # Access the docs for the sum function
...

Elixir还提供了ExDoc工具,利用注释文档生成HTNL页。

你可以看看模块里面列出的模块属性列表, 看看Elixir还支持那些模块属性。

Elixir还是用这些属性来定义typespecs: - @spec 为一个函数提供specification - @callback 为行为回调函数提供spec - @type 定义一个@spec中用到的类型 - @typep 定义一个私有类型,用于@spec - @opaque 定义一个opaque类型用于@spec

本节讲了一些内置的属性。当然,属性可以被开发者、或者被类库扩展来支持自定义的行为。

14.2-作为常量

Elixir开发者经常会模块属性当作常量使用:

defmodule MyServer do
  @initial_state %{host: "147.0.0.1", port: 3456}
  IO.inspect @initial_state
end

不同于Erlang,默认情况下用户定义的属性不被存储在模块里。属性值仅在编译时存在。 开发者可以通过调用Module.register_attribute/3来使属性的行为更接近Erlang。

访问一个未定义的属性会报警告:

defmodule MyServer do
  @unknown
end
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it to nil before access

最后,属性也可以在函数中被读取: ``` defmodule MyServer do @my_data 14 def first_data, do: @my_data @my_data 13 def second_data, do: @my_data end

MyServer.first_data #=> 14 MyServer.second_data #=> 13


注意在函数内读取某属性读取的是该属性当前值的快照。换句话说,读取的是编译时的值,而非运行时。
我们即将看到,这点使得属性可以作为模块在编译时的临时存储。

### 14.3-作为临时存储
Elixir组织中有一个项目,叫做[Plug](https://github.com/elixir-lang/plug)。这个项目的目标是创建一个Web的库和框架。

>类似于rack?

Plug库允许开发者定义它们自己的plug,可以在一个web服务器上运行:

defmodule MyPlug do use Plug.Builder

plug :set_header plug :send_ok

def set_header(conn, _opts) do put_resp_header(conn, “x-header”, “set”) end

def send_ok(conn, _opts) do send(conn, 200, “ok”) end end

IO.puts “Running MyPlug with Cowboy on http://localhost:4000" Plug.Adapters.Cowboy.http MyPlug, []


上面例子我们用了```plug/1```宏来连接各个在处理请求时会被调用的函数。
在内部,每当你调用```plug/1```时,Plug库把参数存储在@plug属性里。
在模块被编译之前,Plug执行一个回调函数,这个函数定义了处理http请求的方法。
这个方法将顺序执行所有保存在@plug属性里的plugs。

为了理解底层的代码,我们需要宏。因此我们将回顾一下元编程手册里这种模式。
但是这里的焦点是怎样使用属性来存储数据,让开发者得以创建DSL。

另一个例子来自ExUnit框架,它使用模块属性作为注释和存储:

defmodule MyTest do use ExUnit.Case

@tag :external test “contacts external service” do # … end end


ExUnit中,@tag标签被用来注释该测试用例。之后,这些标签可以作为过滤测试用例之用。
例如,你可以避免执行那些被标记成```:external```的测试,因为它们执行起来很慢。


本章带你一窥Elixir元编程的冰山一角,讲解了模块属性在开发中是如何扮演关键角色的。

下一章将讲解结构体和协议。

## 15-结构体

在之前的几章中,我们谈到过图:

iex> map = %{a: 1, b: 2} %{a: 1, b: 2} iex> map[:a] 1 iex> %{map | a: 3} %{a: 3, b: 2}


结构体是基于图的一个扩展。它将默认值,编译时保证和多态性带入Elixir。

定义一个结构体,你只需在某模块中调用```defstruct/1```:

iex> defmodule User do …> defstruct name: “john”, age: 27 …> end {:module, User, <<70, 79, 82, ...>>, {:struct, 0}}

 
 现在可以用```%User()```语法创建这个结构体的“实例”了:

iex> %User{} %User{name: “john”, age: 27} iex> %User{name: “meg”} %User{name: “meg”, age: 27} iex> is_map(%User{}) true


结构体的编译时保证指的是编译时会检查结构体的字段存不存在:

iex> %User{oops: :field} ** (CompileError) iex:3: unknown key :oops for struct User


当讨论图的时候,我们演示了如何访问和修改图现有的字段。结构体也是一样的:

iex> john = %User{} %User{name: “john”, age: 27} iex> john.name “john” iex> meg = %{john | name: “meg”} %User{name: “meg”, age: 27} iex> %{meg | oops: :field} ** (ArgumentError) argument error


通过使用这种修改的语法,虚拟机知道没有新的键增加到图/结构体中,使得图可以在内存中共享它们的结构。
在上面例子中,john和meg共享了相同的键结构。

结构体也能用在模式匹配中,它们保证结构体有相同的类型:

iex> %User{name: name} = john %User{name: “john”, age: 27} iex> name “john” iex> %User{} = %{} ** (MatchError) no match of right hand side value: %{}


匹配能工作,是由于结构体再底层图中有个字段叫```__struct__```:

iex> john.struct User


总之,结构体就是个光秃秃的图外加一个默认字段。我们这里说的光秃秃的图指的是,为图实现的协议都不能用于结构体。
例如,你不能枚举也不能访问一个结构体:

iex> user = %User{} %User{name: “john”, age: 27} iex> user[:name] ** (Protocol.UndefinedError) protocol Access not implemented for %User{age: 27, name: “john”}


结构体也不是字典,因为也不适用字典模块的函数:

iex> Dict.get(%User{}, :name) ** (ArgumentError) unsupported dict: %User{name: “john”, age: 27}


下一章我们将介绍结构体是如何同协议进行交互的。

## 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的可扩展性。<br/>

在前面几章中我们知道,尽管结构体就是图,但是它们和图并不共享各自协议的实现。
像前几章一样,我们先定义一个名为```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中的错误捕捉以及异常。

jack.zh 10.09 Elixir 阅读  2743  次

Fork me on GitHub