elixir notes
此文为《Elixir 程序设计》的阅读笔记。(国庆是过了一下官网的 guide,完全不行,英语水平有待提高啊,只好买本书来看)
第1章 接受现实
-
Elixir 语言用整洁,现代的语法将不可变状态和基于 actor 方式实现并发的特性封装到函数式编程中。并且,它运行在工业级,高性能,分布式的 Erlang 虚拟机里。
-
编程时应该关注数据转换
- 借助管道来组合转换
- 命令管道可以并行工作
- 函数是数据转换器
- 数据转换的理念是函数式编程的核心:函数将输入转换成输出
- 当你不用肩负维护数据状态的责任,而开始关注如何把事情做好时,你所看到的世界可能会有所不同。
-
安装 elixir,我是跟官方的 guide 用 asdf 来安装的。
- iex 辅助函数
- iex 输入 h(按回车键)能获取辅助函数列表
- 编译和运行
- 以 .ex 结尾的文件一般会被编译成字节码来运行,而那些以 .exs 结尾的文件更像是用脚本语言写的程序———— 它们在源码级被高效地解释执行。
-
写代码没有唯一正确的方法。
- 记住,乐而为之。
第2章 模式匹配
-
在 Elixir 里,等号不是赋值,而更像一种断言(assertion)。如果 Elixir 可以找到一种方式让等号的左边等于右边,则执行成功。 Elixir 将等号“=”称为匹配运算符(match operator)。
-
Elixir 列表可以表示为由方括号包含的一组用逗号分隔的值。
-
模式匹配只有在其值与模式的结构相同,而且模式中的项目与值中对应的项目都匹配时才算成功。
-
特殊变量 _(下划线)类似于一般变量,只不过它会立即丢弃提供给它的任何值。在匹配过程中,它就像通配符,声称“我可以接受任何值”。
-
在匹配过程中,变量一旦被绑定为某个值,那么该值在匹配其余部分的时候就会保持不变。然而,在随后的匹配中,变量可以被绑定为新值,并且变量的当前值并不会参与新的匹配。
-
使用 ^(脱字符)前缀可以强制让变量的已有值参与匹配。
-
Elixir 的模式匹配类似于 Erlang 的模式匹配(主要区别是 Elixir 允许已赋值变量在之后的匹配中再次被赋值,而 Erlang 仅允许变量赋值一次)。
-
模式匹配是 Elixir 的核心部分,我们将用它来做条件判断,函数调用(function call)及函数被调用(function invocation)。
-
传统编程语言可能是设计来更好地改变数据的。Elixir 则不同,它的所有数据都是不可变的。
第3章 不可变性
-
实际上在函数式程序里,数据一旦被创建就不能被改变。确实,Elixir 强制使用不可变数据。
-
在大多数编程语言里,大多数复合数据类型是可变的————你可以修改它们的全部或部分内容。而且,如果多段并行的代码都在做这件事,那么受伤的总是你。
-
在 Elixir 中,所有值都是不可变的。最复杂的嵌套列表,数据库记录————这些东西的行为就跟最简单的整型值一样。它们都是不可变的。
-
在 Elixir 里,变量一旦引用了一个列表,如 [1,2,3],你就知道它总是引用相同的值(直到你重新绑定变量为止)。
-
因为 Elixir 知道已有的数据是不可变的,所以当创建新的结构时,它可以重用这些数据的部分或全部。
-
Elixr 最酷的地方,你可以在代码中使用许许多多的进程,而每个进程都有自己的堆。应用程序的数据都由这些进程分摊,所以,跟把所有数据都放到一个堆里的情况相比,每个单独的堆是非常小。
-
你只要记住,任何转换数据的函数都会返回新的数据副本。
-
在函数式语言里,我们总是转换数据。我们从不就地修改它。每次使用这个语法的时候,都要记住这一点。
第4章 Elixir 基础
- 内置类型(还没包括函数,字符串,结构体;正则表达式和区间的底层实现是结构体)
- 值类型
- 任意大小的整数
整数字面量可被写作十进制(1234),十六进制(0xcafe),八进制(0o765)和二进制(0b1010)。 书写数值较大的数字时可以用下划线分组。 整数大小没有限制。
- 浮点数
浮点数使用含小数点的十进制数表示。 小数点前后必须有一位数字。 结尾的指数可选。 浮点数是符合 IEEE 754 标准的双精度表示。
- 原子
原子是常量,用于表示某些东西的名字。 它以冒号(:)开头,其后跟随一个原子单词或者 Elixir 运算符。原子单词由字母,数字,下划线和符号“@”组成,以感叹号或者问号结尾。 原子的名字就是它的值。
- 区间
区间被表示为 开始..结束。 如果想遍历区间中的值,其两端必须是整数。
- 正则表达式
Elixir 支持正则表达式字面量,写作 ~r{regexp} 或者 ~r{regexp}opts。(分隔符可选) 可使用 Regex 模块操作正则表达式。
- 系统类型(对应着 Erlang 虚拟机的底层资源)
- PID 和端口
PID 是本地或者远端进程的引用,而端口是读/写资源(通常是对应用程序外部)的引用。 当前进程的 PID 值可以通过调用 self 获得。
- 引用
make_ref 函数用于创建全局唯一的引用;任何其他引用都不会与之相等。
- 收集类型(可以包含任意类型的值)
- 元组
元组表示一组有序元素的集合。 元组写在一对花括号内,其元素用逗号隔开。 通常 Elixir 元组由两到四个元素组成。
- 列表
列表是一个链式数据结构。 列表要么为空,要么由首部和尾部组成。首部包含一个值,尾部本身是一个列表。 列表适合线性遍历,随机访问会很费时。
- 关键字列表
因为我们常常需要一组键值对,Elixir 提供了一个简写方式。[ a: 'a', b: 'b' ] Elixir 会将其转换为一个由双值元组组成的列表。[ { :a, 'a' }, { :b, 'b' }] 当关键字作为函数调用的最后一个参数时, Elixir 允许去掉方括号。 如果关键字列表在任意期望列表值得上下文中作为最后一项出现,我们也可以省略列表的括号。
- 散列表
散列表是一个键值对的集合。散列表的字面量形如: %{ key => value, key => value } 通常情况下,同一散列表中键类型相同,但也不尽然。 如果键是原子类型,可以使用类似于关键字列表的简略写法。 散列表的每个条目的键是不同的,而关键字列表允许存在重复的键。 通常,我们用关键字列表来处理命令行参数和传递选项,而想获得关联数组的时候就用散列表(或 HashDict) 散列表可以通过键获取值。方括号语法对所有散列表都有效。 如果键是原子类型,还可以使用点符号。使用点符号的时候,如果没有匹配的键,将得到一个 keyError。
- 二进制型
二进制字面量被放在 << 和 >> 之间。此基本表示法将连续的整数逐个转换成字节。 也可以通过修饰符控制类型和每个字段的大小。
- 值类型
-
命名,源文件,约定,运算符和其他
- Elixir 的标识符由大小写字母,数字和下划线组成。可以以问号或者感叹号结尾。
- 模块,记录,协议和行为的名称以大写字母开头并使用大驼峰命名法。其他标识符以小写字母或者下划线开头,通常用下划线分割单词。如果首字母是下划线,若变量在模式匹配和函数参数列表中未被使用,Elixir 不会发出警告。
- 源文件使用 UTF-8 编码,但是标识符只能用 ASCII 字符。
- 源文件采用两格空格缩进。
- 注释以井号(#)开始,直至行尾。
- 真值
- true,false 和 nil(nil在布尔上下文会被当做 false 来对待)。
- 这三个值都是与之同名的原子类型的值。
- 比较运算符
===
!==
==
!=
>
>=
<
<=
- 可以比较任意类型的值。
-
布尔运算符:
or
and
not
-
松弛布尔运算符:
||
&&
!
-
算术运算符:
+
-
*
/
div
rem
-
连接运算符
<>
++
--
- in 运算符
in
第5章 匿名函数
- 匿名函数用
fn
关键字创建。可以把fn...end
当作像括着字符串字面量的双引号,只不过返回的值是函数,而不是字符串。
fn
parameter-list -> body
parameter-list -> body
end
-
点符号表示函数调用,括号中是被传递的参数。例如
sum.(1,2)
。命名函数不需要使用点,这是命名函数和匿名函数的区别。如果函数不接受参数,你还是要用圆括号来调用它,不过可以在函数定义中省略圆括号。 -
Elixir 没有赋值的概念,而是将值与模式进行匹配。
-
在单个函数定义中,你可以定义不同的实现,这取决于传入的参数类型和内容。(你不能根据参数数目进行选择————函数定义中的每个子句必须拥有相同数目的参数。)简单来说,我们可以用模式匹配来选择要运行的子句。
-
你可以访问现有的全部 Erlang 库,但是需要在使用它们的时候,区别对待 Erlang 函数和 Elixir 函数。
-
Elixir 的字符串插值:在字符串里,
#{...}
里面的内容会被求值,并且求值结果会被替换成字符串。 -
Elixir 函数带有其定义所在作用域的变量绑定。
-
闭包:作用域将其中的变量绑定封闭起来,并将它们打包到稍后能被保存且使用的东西上。
-
函数就是值,所以我们可以将它们传递给其他函数。
-
&
运算符将跟在其后的表达式转换成函数,在表达式内部,占位符&1
,&2
等对应第一个,第二个及接下来的函数参数。所以,&(&1 + &2)
会被转换成fn p1, p2 -> p1 + p2 end
。 -
因为在 Elixir 里
[]
和{}
是运算符,所以字面量的列表和元组也可以被转换成函数。 -
&
函数捕获运算符还有一种形式。你可以给它一个已存在的函数名称和形参数量,它会返回一个调用该函数的匿名函数。传递给该匿名函数的参数会按顺序被传递给命名函数。 -
&
快捷方式是一种将函数传递给其他函数极好的方法。
第6章 模块与命名函数
-
Elixir 的命名函数必须写在模块里。
-
参数个数也是 Elixir 函数标识符的一部分,所以我们看到函数名被写成
double/1
-
装载源文件到 iex 有两种方法 1.
iex xxx.exs
将源文件名传给 iex 2. 进入 iex,通过 c 命令编译文件而不用退回命令行。iex> c "xxx.exs"
-
Elixir 里命名函数的标识为名字加上参数个数(它的 arity)。
-
代码块
do...end
是一种表达式的组织方式,可被传递给其他代码。它被用在模块,命名函数的定义,控制结构等 Elixir 中任何需要将代码作为整体来对待的地方。它的真是语法类似于:def double(n), do: n * 2
。通过圆括号可以给do:
传递多条语句。 -
do...end
形式就是一块语法糖————编译期间被转换为do:
形式(并且do:
形式也没什么特别的,就是关键字列表的一项而已)。通常,在单行代码使用do:
语法,在多行代码使用do...end
语法。 -
命名函数同样支持通过模式匹配绑定形参和实参。命名函数会被书写多次,但每次的参数列表和函数体不同。虽然看起来像定义多个函数,但纯粹主义者会告诉你它们不过是同一个定义的多个子句而已(并且他们是对的)。
-
当我们写代码时,子句顺序不同会产生不同的结果。Elixir 会自上而下依次尝试,执行最先匹配到的一项。
-
哨兵子句(guard clause)是一个或多个由
when
关键字紧接在函数定义之后的断言。当执行模式匹配,Elixir 首先执行普通的基于参数的匹配,然后评估所有的when
断言,仅当至少有一条断言为真时才执行函数。 -
哨兵子句仅支持 Elixir 表达式的一个子集。
-
当你定义命名函数时,可以用
param \\ value
的语法给任意参数指定默认值。 -
defp
宏用来定义私有函数————该函数仅能在声明它的模块内被调用。 -
|>
运算符获得左边表达式的结果,并将其作为第一个参数传递给右边的函数调用。val |> f(a, b)
等价于f(val, a, b)
。 -
&
快捷标记和管道符号会冲突,所以使用&
的地方必须使用圆括号,即是,在管道中我们要用圆括号将函数的参数列表括起来。 -
编程就是转换数据,而
|>
运算符让转换更直接。 -
模块为你定义的内容提供了命名空间。我们已经知道它们可以用来封装命名函数。它们还可以封装宏,结构体,协议和其他模块。如果想在某个模块的外部引用该模块里的函数,只需要加上模块名作为前缀。如果要在代码里引用同一模块里的东西,不需要加前缀。
-
Elixir 也可以通过嵌套模块来改进结构的可读性和复用性。在外部访问嵌套模块内的函数,需要加上所有的模块名作为前缀。在模块内部访问它时,要么使用完全限定名,要么使用内部模块名作为前缀。
-
Elixir 里嵌套模块只是一个假象————所有模块都定义在顶层。当我们在某个模块内部定义模块时,Elixir 仅仅在内部模块名前加上外部模块名,两者之间加上点号。这意味着你可以自己定义内部模块。
- Elixir 提供三个指令简化模块的使用。这三个指令都在程序运行期执行,并且都在词法作用域内有效————以指令出现处作为起点,知道当前作用域结束。
import
指令将模块内的函数和/或宏引入到当前作用域。完整语法为:import Module [, only:|except: ]
alias
指令为模块创建别名。alias Mix.Tasks.Doctest, as: Doctest
。as:
参数默认为模块名字的最后一部分。- 当想用某个模块中定义的宏时,
require
那个模块。require
指令确保在代码使用宏之前,加载定义这些宏的模块。
-
每个 Elixir 模块都有与之关联的元数据。元数据的每一项称为模块的属性,并且有自己的名字。在模块内部,可以通过在其名称前加
@
符号访问这些属性。可以在模块顶层用@name value
来赋值这些属性,在函数内部不能设置属性,不过在函数内部可以访问属性。在模块内能为属性多次赋值。如果在模块内的命名函数内部访问该属性,其值事实上是定义函数时属性的当前值。这些属性不是传统意义上的变量。它们仅作为配置和元数据使用。 -
在内部,模块名仅仅是原子类型。调用一个模块内部的函数,事实上就是一个原子后面加一个点再加一个函数名。
-
Erlang 有不同的命名约定————变量以大写字母打头,而原子的名字都是小写字母。
- 如果你正在为应用程序寻找函数库,最好先快速浏览 Elixir 已有的模块。内置模块的文档在 Elixir 官网上,其他的列在 hex.pm 和 Github 上(搜索 elixir)。如果没有找到,搜索 Erlang 的内置函数库或在网站上搜索。
第7章 列表与递归
-
递归才是处理列表的最佳工具
-
我们可以使用管道运算符
|
来分隔头部和尾部。可以在模式里面使用管道运算符,那么,管道运算符的左边会匹配列表的头部值,而其右边会匹配列表的尾部。 -
每个列表都以空列表结束,所以
tail
可以是[]
。 -
在模式中也可以这样用,你可以匹配多个单独的元素作为头部。
-
List 模块实践:
[] ++ []
List.flatten([[]], [])
List.foldl
List.foldr
List.zip
List.unzip
List.keyfind
List.keydelete
List.keyreplace
第8章 字典:散列表,散列字典,关键字,列表,集合与结构体
-
字典是将键值关联起来的数据结构。
-
需要将多个条目对应同一个键 =》 需要使用 Keyword 模块实践; 需要保证元素的次序 =》需要选择 Keyword 模块; 需要对内容进行模式匹配 =》使用散列表; 需要存储数百个的条目 =》使用 HashDict。
-
散列表和散列字典都实现了 Dict 的行为。Keyword 模块也基本实现了,不同之处在于它支持重复键。
-
Enum.into
可以方便地把一种类型的收集映射成另一种。 -
散列表
%{ a: 1 }
[a: 1] |> Enum.into Map.new
; 散列字典[ a: 1 ] |> Enum.into HashDict.new
-
散列表在模式匹配的时候不允许将一个值绑定到键。
-
更新散列表的最简单的方式是使用如下语法:
new_map = %{ old_map | key => value, ... }
-
结构体就是模块,它封装了一个有限形式的散列表。有限是因为键必须为原子,并且这些散列表不具备 Dict 和 Access 的特性。这个模块名称就变为散列表的类型名称。在模块内部,使用
defstruct
宏来定义散列表的性质。
defmodule Subscriber do
defstruct name: '', paid: false, over_18: true
end
s1 = %Subscriber{}
s1.name
%Subscriber{ s1 | name: 'name' }
-
创建结构体的语法和创建散列表的语法一样————在
%
和{
之间加上模块名即可。可以通过点标记和模式匹配访问结构的字段。 -
可以在模块里面添加结构体专用的行为。
-
字典是可以嵌套的,
put_in
函数可以设定嵌套结构里的值,update_in
函数让我们可以在结构体的某个值上执行一个函数。另外两个嵌套访问函数时get_in
和get_and_update_in
。 -
get_in
,put_in
,update_in
和get_and_update_in
都能接受键列表作为单独参数。添加这样的参数后,这几个宏就变成函数调用了。 -
当前集合只有一个实现: HashSet。
第9章 番外篇————类型是什么
- 原生数据类型并不一定要与它们展示出来的形式一样。
第10章 处理收集————Enum 与 Stream
-
从技术上讲,可被遍历的类型都被认为是实现了 Enumerable 协议的。
-
Enum 模块是收集的骨干部分。Stream 模块让你的遍历收集操作延迟执行。
-
Enum 模块的方法:
Enum.to_list
Enum.concat
Enum.map
Enum.at
Enum.filter
Enum.reject
Enum.sort
Enum.max
Enum.max_by
Enum.take
Enum.take_every
Enum.take_while
Enum.split
Enum.split_while
Enum.join
Enum.all?
Enum.any?
Enum.member?
Enum.empty?
Enum.zip
Enum.with_index
Enum.reduce
-
由于流是可枚举的,你也可以将流传递给流函数。正因为这样,我们说流是可组合的。
-
链式的流表现得就像一系列函数,在处理的过程中它们依次应用于流中的每个元素。
-
Stream 模块的方法:
Stream.cycle
Stream.repeatedly
Stream.iterate
Stream.unfold
Stream.resource
-
Collectable 允许你通过插入元素构建一个收集。
-
推导式的概念非常简单:给定一个或多个收集,从每个收集中提取所有值的组合,选择性地过滤某些值,然后使用剩下的值生成一个新的集合。语法:
result = for generator or filter ... [, into: value ], do: expression
生成器指定要如何从一个收集中提取值:patern <- list
。过滤器是一种断言。into:
可以指定一个收集,该收集接受推导式的结果。into:
选项接受实现 collectable 协议的值,包括列表,二进制型,函数,散列表,文件,散列字典,散列集合和 IO 流。 -
推导式也支持二进制位,不过语法有改变,生成器被括在
<<
和>>
之间,表示是一个二进制型数据。 -
所有在推导式内部赋值的变量只在推导式内部有效————不会对作用域外部的变量产生影响。
第11章 字符串与二进制型
-
Elixir 有两种字符串:用单引号括起来的和用双引号括起来的。它们最主要的区别是内部表示形式。但它们也有很多共同点:字符串支持 UTF-8 编码的字符;可以包含转义字符;允许在 Elixir 表达式中通过语法
#{..}
来插值;可使用反斜杠对有特殊意义的字符进行转义;支持 heredoc。 -
输入三个字符串分隔符(
'''
或"""
)并将末尾的分隔符进到与你的字符串内容一样的边距。heredoc 被广泛用于给函数和模块添加文档。 - 在 Elixir 里,这些
~
类型的字面量称为魔术符(一个具有魔力的符号)。魔术符以波浪符开头,后接一个大写或小写字母,然后是一些用分隔符限定的内容,可能还有一些选项。分隔符可以是<...>
,{...}
,[...]
,(...)
,|...|
,/.../
,"..."
或'...'
。其中的字母决定魔术符的类型:~C
字符列表,不支持转义或插值~c
字符列表,支持类似单引号字符串的转义和插值~R
正则表达式,不支持转义或插值~r
正则表达式,支持转义和插值~S
字符串,不支持转义或插值~s
字符串,类似双引号字符串,支持转义和插值~W
以空格分隔的单词列表,不支持转义或插值~w
以空格分隔的单词列表,支持转义和插值~W
和~w
魔术符接受一个可选的类型选择符:a
,c
或s
,它们分别决定要返回的类型是原子类型,列表还是字符串。 Elixir 不检查嵌套分隔符。
-
Elixir 的约定是,我们只会称双引号字符串为“字符串”。单引号的形式是字符列表。用于字符串的库也只适用于双引号形式。
-
单引号字符串会被表示成整数值列表,么个值对应字符串中的一个编码点。因此,我们把它们称为字符列表。如果列表中每个数字都是可打印字符时,iex 会将这个证书列表以字符串形式打印出来。
-
二进制型表示位的序列。二进制型的字面值是这样子的:
<< term,... >>
。你可以通过指定修饰符来设定任意项的大小(以二进制位为单位)。你可以在二进制型中存储整数,浮点数和其他二进制数据。 -
双引号字符串的内容是以 UTF-8 编码存储的连续字节序列,而单引号字符串则存储为字符列表。
-
String 模块定义了很多处理双引号字符串的函数。
at(str, offset)
,capitalize(str)
,codepoints(str)
,downcase(str)
,duplicate(str, n)
,ends_with?(str, suffix | [ suffixes ])
,first(str)
,graphemes(str)
,last(str)
,length(str)
,ljust(str, new_length, padding \\ " ")
,lstrip(str)
,lstrip(str, character)
,next_codepoint(str)
,next_grapheme(str)
,printable?(str)
,replace(str, pattern, replacement, options \\[global: true, insert_replaced: nil])
,reverse(str)
,rjust(str, new_length, padding \\ " ")
,rstrip(str)
,rstrip(str, character)
,slice(str, offset, len)
,split(str, pattern \\ nil, options \\ [global: true])
,starts_with?(str, prefix | [prefixes])
,strip(str)
,strip(str, character)
,upcase(str)
,valid_character?(str)
-
二进制型的第一法则是“如果有疑问,就给每个字段指定类型”。可用的类型有 binary, bits, bitstring, bytes, float, integer, utf8, utf16 和 utf32。限定符有
size(n)
signed
unsigend
big
little
native
。 - 我们可以指定头部(UTF-8)的类型,并确保尾部依然是二进制型来处理存放了字符串的二进制型。
第12章 控制流
-
在 Elixir 里,我们编写许多小函数,并联合使用哨兵子句和参数的模式匹配,替代了大多数其他语言中所见的控制流。
-
Elixir 代码倾向于声明式而非命令式。
-
if
和unless
接受两个参数:一个条件和一个关键字列表,该列表页包括关键词do:
和else:
。if
和unless
的值是被执行表达式的值。 -
cond
宏允许列出一系列条件,每个条件对应一段代码。第一个满足的条件所对应的代码会被执行。 -
case
测试一组模式,执行第一个匹配成功的模式所对应的代码,并返回代码的值。模式可以包含哨兵子句。 -
官方提醒:Elixir 里的异常不属于控制流结构。相反,Elixir 异常用于处理正常运行下不应该发生情况。
raise
函数抛出异常。
第13章 组织项目(此章更多在于实践)
-
mix
是管理 Elixir 项目的命令行工具,用来创建新项目,管理项目依赖关系,做测试和运行你的代码。mix help
mix help deps
mix new project_name
mix test
mix run -e ''
mix deps
mix deps.get
iex -S mix
mix escript.build
mix docs
-
寻找库:
http://elixir-lang.org/docs/
http://hex.pm
,mix 也可以从 github 加载依赖。
第14章 运用多进程
-
Elixir 的一个重要的特征是将代码打包成可独立和并发运行的小块。
-
Elixir 使用 actor 并发模型。actor 是一个无依赖的进程,它不与其他进程共享任何东西。你可以
spawn
新进程,向它发消息,并且用receive
接收消息,仅此而已。 -
spawn
函数创建一个新进程。spawn
返回一个进程标识符,通常叫做 PID,用来唯一标识它创建的进程。当调用spawn
的时候,它会调用一个新进程来运行我们指定的代码。 -
应该使用消息同步进程的活动。
-
在 Elixir 里使用
send
函数发送消息。它接受一个 PID 和在右边的要发送的消息(一个 Elixir 值,也叫作 term)。你可以发送任何值,但是大多数 Elixir 开发者更愿意用原子和元组。 -
等待消息则使用
receive
。在某种程度上,它的用法像case
,带着消息体作为参数。receive
代码块的内部,我们能指定任意数量的模式和所对应的动作。就像case
,第一个与函数匹配的模式所对应的动作会被执行那样。 -
self
函数返回其调用者的 PID 值。 -
after
的伪模式可以告诉receive
,如果消息在指定的时间(单位为毫秒)内未抵达就超时。 -
Elixir 实现了尾递归优化。如果一个函数最后一件事是调用自己,那就没有必要调用。相反,运行时可以简单地跳转到函数开始的位置。如果递归调用有参数,那么它们替换掉原始的参数,就像循环的过程一样。注意:递归调用必须是最后一个执行的。
-
哨兵子句同样可以用在
receive
块来限定模式。 -
如果两个进程想互相分担负载,我们可以关联(link)他们。当两个进程被关联,任何一个都会接收到另一个退出的信息。
spawn_link
调用把创建一个进程和与调用者关联合并成一个操作。关联进程的默认行为————当其中一个进程非正常退出时,会把另一个进程也杀死。 -
监控让一个进程创建另一个进程,并且能接收到其退出的消息,反之则不然————这种监控只是单向的。当创建进程时,你可以用
spawn_monitor
开启监控,或者使用 Process。monitor 监控已存在的进程。 -
spawn_link
和spawn_monitor
版本的操作是符合原子性的。 -
如果想在其中一个出错的时候终止另一个,那就需要使用链接。如果想知道某个进程因为什么退出,就选择监控吧。
-
普通 map 返回列表,该列表是某个收集的每个元素应用于某个函数的结果。并行版本做同样的事情,但是每个元素在独立的进程里应用于函数。
-
Elixir 库有一个叫做 Agent 的模块,它可以方便地将包含状态的进程封装在一个好的模块接口里。
第15章 节点————分布式服务的关键
-
节点就是一个运行中的 Erlang 虚拟机。
-
这个 Erlang 虚拟机,被称为 Beam,更像是一个简单的解释器。它就像运行在你主机操作系统上的小型操作系统。它负责管理自身的事件,进程调度,内存,名字服务及进程内通信。除此之外,一个节点可以连接到其他节点中————在同一台机器,同一个内网或互联网上————并且它能跨越这些连接提供许多跟本地进程一样的进程。
-
Node.self
Node.list
Node.connect
iex --name
iex --sname
Node.spawn
iex --cookie
:global.register
:global.whereis_name
:erlang.group_leader
-
在一个节点允许其他节点访问前,它会检查远程节点的权限。它会将那个节点的 cookie 与自己的进行比较。cookie 只是一个任意的字符串(理想情况下,它是很长的,非常随机的字符串)。作为分布式 Elixir 系统的管理员,你需要创建 cookie,并确保所有的节点使用它。
-
在 Erlang 启动的时候,它会在你的主目录查找 .erlang.cookie 文件。如果这个文件不存在,Erlang 会创建一个,并向里面存入一个随机字符串。它会对该用户启动的任何节点都使用这个字符串作为 cookie。那样的话,你再某台机器上启动的所有节点就自动地允许相互访问了。
-
要注意通过公共网络连接节点的情况————cookie是以明文传输的。
-
PID 是以三个数字表示的,但它只包含两个域:第一个数字时节点 ID,接下来两个数字时进程 ID 的低位和高位。当你再当前节点运行一个进程时,它的节点 ID 总是零。然而,当你将PID导出到另一个节点,节点 ID 就会被设置成进程所在节点的数字。
-
如果你想在一个节点上注册回调进程而在另一个节点注册时间产生进程,只需要把回调进程的 PID 提供给生成器即可。
-
Erlang 虚拟机中的输入和输出都是由 I/O 服务器来完成的。这些服务器只不过是实现了底层消息接口的 Erlang 进程。
-
在 Elixir 里,通过其 I/O 服务器的 PID 来识别一个打开的文件或设备。而这些 PID 的行为和其他所有的 PID 一样。
第16章 OTP: 服务器
-
OTP 经常被渲染为解决所有高可用性分布式应用程序困境的法宝。未必,但它肯定可以解决许多本来需要你自己解决的问题,包括服务发现,故障检测与管理,热代码交换和服务器结构安排。
-
OTP 表示开放电信平台(Open Telecom Paltform)。
-
OTP 实际上是一个包,其中包括 Erlang,数据库(被美妙地称为 Mnesia)和不计其数的库文件。它还定义了应用程序的结构。
-
OTP 按照应用程序的层次来定义系统。一个应用程序由一个或多个进程组成。这些进程遵循少数 OTP 约定之一,这些约定被称为行为(behaviors 或 behaviours)。一种行为用于通用服务器,一种用于事件处理器,还有一种用于有限状态机。这些行为的实现都运行在它们各自所属的进程里(并且可能伴随着相关联的附属进程)。
-
当我们实现 OTP 服务器时,我们在模块里包含若干个约定名称的回调函数。OTP 会调用合适的回调函数来处理特定的情况。
-
GenServer.start_link
GenServer.call
GenServer.cast
-
start_link
的第三个参数是一组可选项。 -
Erlang 的 sys 模块是你与系统消息世界连接的接口。
-
GenServer 是一个 OTP 协议。OTP 起作用的前提是,假设模块定义了许多回调函数(GerServer 有 6 个
init(start_arguments)
handle_call(request, from, state)
handle_cast(request, state)
handle_info(info, state)
terminate(reason, state)
code_change(from_version, state, extra)
format_status(reason, [pdict, state])
)。响应:{ :noreply, new_state [, :hibernate | timeout ]}
{ :stop, reason, new_state }
{ :reply, response, new_state [ , :hibernate | timeout ] }
{ :stop, reason, reply, new_state }
-
创建本地命名进程,启动服务器的时候需要加上 :name 参数。
第17章 OTP:应用程序监视器
-
Elixir 编程方法是不太担心部分代码崩溃的;反而要确保整个应用程序能一直运行。
-
在 Elixir 和 OTP 的世界里,应用程序监视器负责执行所有进程的监视与重启的工作。Elixir 应用程序监视器的存在只为一个目的————管理一个或多个工作进程。简单地说,应用程序监视器就是一个使用 OTP 应用程序监视行为的进程。它会得到一个要监控的进程列表,并被告知在进程挂掉后该做什么及如何避免重启循环。
-
mix new --sup app_name
第18章 OTP:应用程序
-
在 OTP 的世界里,应用程序是一组带描述符的代码。描述符说明运行时依赖什么代码,全局注册的名称,等等。事实上,OTP 应用程序更像动态链接库或共享对象而不是通常意义上的应用程序。
-
在使用 mix 的过程中经常会讨论一个名为
name.app
的文件,其中 name 是应用程序的名字。这个文件被称为应用程序规范(application specification),用来定义应用程序的运行环境。mix 会借助于 mix.exs 和编译过程中收集的信息自动创建这个文件。当应用程序运行的时候,用根据这个文件来决定需要装载什么。
第19章 任务与代理
-
任务和代理是两个易于使用的 Elixir 抽象。它们使用了 OTP 的特性,但又不需要我们处理细节。
-
Elixir 任务就是一个在后台运行的函数。调用
Task.async
创建一个运行给定函数的独立进程。async
的返回值是任务描述符(其实就是一个 PID 和一个引用),后面我们可以用它来识别任务。调用Task.await
,传入任务描述符,会等待我们的后台执行完毕并返回它的值。 -
任务被实现是 OTP 服务器,这意味着我们可以将其添加到应用程序监视树中。
-
代理是一个维护状态的后台进程。这个状态可在不同地方访问,包括进程,节点以及跨越多个节点。初始状态是在启动代码时由我们传入的函数设置的。我们可以使用
Agent.get
来获取状态,向其传入代理描述符及一个函数。该代理用当前的状态来运行此函数并返回运行结果。我们也可以使用Agent.update
来改变代理所保存的状态。 -
何时使用代理和任务,何时使用 GenServer ? 答案是使用最简单且可行的方法。在处理针对特定后台的活动时,代理和任务非常棒,而 GenServer(顾名思义)则更通用。
-
第20章 宏与代码求值
-
⚠️:使用宏容易让代码难以理解,因为你基本上重写了 Elixir 的各个部分。出于这个原因,可以使用函数时,千万不要用宏。
-
Elixir 的内部表示是一个嵌套的 Elixir 元组。
-
defmacro
用于定义一个宏。当我们给宏传递参数时,Elixir 不会计算这些参数的值。相反,Elixir 会将参数代码以元组的形式传递。 -
宏在程序运行前展开,在一个模块中定义的宏,当 Elixir 编译另一个依赖它的模块时,必须处于可用状态。
require
函数告诉 Elixir 要确保指定模块在当前模块之前已被编译。实际上,它的作用就是让一个模块中定义的宏在另一模块中可用。 -
quote
可以强制代码保留未求值的形式。quote
接受一个代码块,返回该代码块的内部表示。 -
unquote
只能在quote
块的内部使用。在quote
块内部,Elixir 忙于解析代码并生成内部表示。但是当它遇到unquote
,就会停止解析,仅仅将 code 参数复制到生成的代码里。在unquote
之后,Elixir 又开始正常地解析代码。 -
如果我们仅想插入列表的元素,可以使用
unquote_splicing
。 -
将值注入到
quote
代码块的两种方法。一种是unquote
,另一种是使用绑定。 -
绑定不过是变量名称和它们值的一个关键字列表。
-
bind_quoted
接受一个quote
代码片段。 -
宏定义既有自身的作用域,也有被
quote
的宏体在运行期间的作用域。 -
用
Code.eval_quoted
来执行代码片段,比如那些quote
返回的代码。 -
用
Code.string_to_quoted
将包含代码的字符串转换为quote
形式,而Macro.to_string
将代码片段转换会字符串。使用Code.eval_string
直接对字符串求值。 -
宏定义是受词法作用域限制的。
-
Code
和Macro
模块包含维护代码内部表示的函数。 -
第21章 连接多个模块:行为与 use
-
Elixir 的行为只不过是一个函数列表。一个模块如果声明自己实现特定行为,就必须实现所有相关的函数。
-
我们使用 Elixir 的
Behaviour
模块以及defcallback
来定义行为。 -
定义了行为之后,我们就可以在其他实现该行为的模块中使用
@behaviour
属性来声明。 -
在某种意义上说,
use
是一个平常的函数。你给它传入模块及可选的参数,它就在那个模块里调用函数或宏__using__
,并将参数传给它们。 -
第22章 协议————多态函数
-
在 Elixir 里有协议(protocol)的概念。协议有点像我们在前面章节见到的行为,行为定义了为达到某些目的而必须提供的函数。但是行为是模块内部的————模块实现行为。协议不一样————你完全可以将协议的实现置于模块的外部。
-
defimpl
宏允许给 Elixir 的若干种类型的协议提供实现。在幕后,defimpl
将每个协议和类型的实现组合进独立的模块。 -
可以定义下面若干种类型的实现:
Any Atom BitString Float Function Integer List PID Port Record Reference Tuple
-
结构体的值事实上就是一个散列表,包含一个
__struct__
键,引用了结构体的模块,然后其余元素是该实例的键和值。 -
内置协议:
Access Enumerable String.Chars Inspect
-
第23章 更酷的玩意儿
-
当编写像
~s{...}
这样的魔术符时,Elixir 会将其转换成对函数sigil_s
的调用。它给这个函数传入两个值。第一个是分隔符之间的字符串。第二个是一个列表,包含紧跟在分隔符后的任意小写字符。(第二个参数用于获取你传给正则字面量的选项,如~r/cat/if
)。 -
Elixir 称这些多应用项目为
umbrella
项目(即大型项目)。 -
我们使用
mix new
,传给它--umbrella
选项,来创建 umbrella 项目。子项目就是常规的 mix 项目,这意味着你不用操心是否要新建一个使用 umbrella 的项目,而是直接开始一个简单的项目。如果之后发现有使用 umbrella 项目的需要,就可以创建它,并将现有的简单项目移到 apps 目录中。