Elixir,[ɪ'lɪksər],意为灵丹妙药、圣水,其logo是一枚紫色水滴
Elixir创始人José Valim是ruby界的知名人士。 可以把Elixir看作函数式的ruby语言,或者是语法类似ruby的Erlang。
Elixir受瞩目的主要原因,是因为它较好地结合了Erlang编程语言的各种优点,
以及ruby那样简单易懂的语法(Erlang语法比较晦涩)。
Elixir还是一门初出茅庐的语言:
- 2014年9月1日临晨,1.0.0rc1发布
- 2014年9月7日晚,1.0.0rc2发布
- 2014年9月18日,v1.0正式发布
- 2015年9月28日,v1.1发布
- 2016年1月1日,v1.2发布
- 2016年6月2日,v1.3发布
Elixir 是一种动态的函数编程语言,用来编写可扩展和易于维护的应用。
函数式编程是种编程方式,它将电脑运算视为函数的计算。
Elixir 语言运行在Erlang虚拟机上的一门新语言,可以很方便地编写分布式、低延迟和高容错性的系统。
- 高并发
- 高容错性
- 函数式编程
- 可扩展
安装环境
mac上安装Elixir
我的电脑是mac,所以这里列出mac的安装过程。windows、linux、os的详细安装在官方文档有详细说明。
- 安装erlang
brew install erlang
- 安装elixir
brew install elixir
在控制台输入elixir -v可以查看当前安装的elixir版本(我这里安装的是最新版本1.6.1)
Erlang/OTP 20 [erts-9.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir 1.6.1 (compiled with OTP 20)
安装好Elixir之后,你有了三个可执行文件:iex,elixir和elixirc。
如果你是用预编译包方式安装的,可以在解压后的bin目录下找到它们。
交互模式,类似于Ruby的irb,就是可以向其中输入任何Elixir表达式或命令,然后直接看到表达式或命令的结果。 控制台敲命令iex
会显示:
Erlang/OTP 20 [erts-9.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.6.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
如果你用的是Windows,你可以使用iex.bat来进入交互模式
永远的hello world
iex(1)> "hello world"
"hello world"
iex(2)> "hello
...(2)> world"
"hello\nworld"
iex(3)> "hello" <> " world"
"hello world"
当然,也可以把表达式写进脚本文件,用elixir命令执行它,如:
vim simple.exs #创建一个名字叫simple,以exs结尾的文件
IO.puts "hello word"
然后执行elixir simple.exs
就可以在控制台显示hello world了
基本类型
Elixir主要的基本类型有: 整型(integer),浮点(float),布尔(boolean),原子(atom,又称symbol符号),
字符串(string),列表(list)和元组(tuple)等。
iex> 1 # integer
iex> 0x1F # integer
iex> 1.0 # float
iex> true # boolean
iex> :atom # atom / symbol
iex> "elixir" # string
iex> [1, 2, 3] # list
iex> {1, 2, 3} # tuple
整数类型
iex(1)> 111
111
浮点类型
iex(2)> 3.14
3.14
布尔类型
Elixir使用true
和false
两个布尔值
iex(3)> true
true
iex(4)> false
false
Elixir提供了许多用以判断类型的函数,如is_boolean/1
函数可以用来检查参数是不是布尔型。
在Elixir中,函数通过名称和参数个数(又称元数,arity)来识别。 如
is_boolean/1
表示名为is_boolean
,接受一个参数的函数; 而is_boolean/2表示与其同名、但接受2个参数的不同函数。(只是打个比方,这样的is_boolean实际上不存在)
另外,<函数名>/<元数>
这样的表述是为了在讲述函数时方便,在实际程序中如果调用函数, 是不用注明/1
或/2
的。
iex(1)> is_boolean(true)
true
iex(2)> is_boolean(1)
false
类似的函数还有is_integer/1
,is_float/1
,is_number/1
, 分别测试参数是否是整型、浮点型或者两者其一。
原子
原子(atom)是一种常量,名字就是它的值。有些语言中称其为符号(symbol),比如ruby。
iex(3)> :hello
:hello
iex(4)> :hello == :hi
false
布尔值true
和false
实际上就是原子:
iex(5)> true == :true
true
iex(6)> is_atom(false)
true
字符串
在Elixir中,字符串以 双括号 包裹,采用UTF-8编码:
iex(5)> "hello world"
"hello world"
字符串支持换行符和转义字符
iex(6)> "hello
...(6)> world"
"hello\nworld"
你可以使用IO
模块(module)里的IO.puts/1
方法打印字符串:
iex(7)> IO.puts "hello\nworld"
hello
world
:ok
函数IO.puts/1
打印完字符串后,返回原子值:ok
。
我们可以使用专门的函数来返回字符串中的字符数量:
iex(8)> String.length("hello world")
11
String模块中提供了很多符合Unicode标准的函数来操作字符串。如:
iex(9)> String.upcase("hello")
"HELLO"
这里有一个注意的点,单引号和双引号包裹的字符串在Elixir中是两种不同的数据类型:
iex(10)> 'hello' == "hello"
false
(链式)列表
Elixir使用方括号标识列表。列表可以包含任意类型的值:
iex(12)> [1, 2, true, 3]
[1, 2, true, 3]
iex(13)> length [1, 2, 3]
3
两个列表可以使用++/2
拼接,使用--/2
做“减法”:
iex(15)> [1, 2, 3, true]
[1, 2, 3, true]
iex(16)> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex(17)> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]
涉及列表头(head)和尾(tail)的概念。
列表的头指的是第一个元素,而尾指的是除了第一个元素以外,其它元素组成的列表。
它们分别可以用函数hd/1和tl/1从原列表中取出:
iex(18)> list = [1, 2, 3]
[1, 2, 3]
iex(19)> hd(list)
1
iex(20)> tl(list)
[2, 3]
尝试从一个空列表中取出头或尾将会报错:
iex(21)> hd []
** (ArgumentError) argument error
:erlang.hd([])
"Use the source, Luke!"
Elixir没有令人生畏的语法,融合了Ruby和Erlang优秀的特性。它不是Erlang也不是Ruby,它有自己创新的想法。
一:基本操作
算数运算
Elixir 支持基本的 +
、-
、*
、/
操作符,不过要注意 /
的结果是浮点数。
iex(1)> 1 + 1
2
iex(2)> 2 - 1
1
iex(3)> 2 * 5
10
iex(4)> 10 / 2
5.0
10 / 2返回了一个浮点型的5.0而非整型的5,这是因为在Elixir中,/
运算符总是返回浮点型数值。
如果你需要整数除法和求余,Elixir 提供了两个函数:
iex(5)> div(10, 2)
5
iex(6)> rem(10, 3) #rem的意思是division remainder,余数的意思
1
iex(7)> rem 10, 3 #在写函数参数的时候,括号是可选的
1
Elixir还提供了++
和--
运算符来操作列表:
iex(1)> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex(2)> [1, 2, 3] -- [2]
[1, 3]
Elixir还提供了三个布尔运算符:or
,and
,not
。这三个运算符只接受布尔值作为 第一个 参数:
iex(3)> true and true
true
iex(4)> false or is_atom(:example)
true
如果提供了非布尔值作为第一个参数,会报异常:
iex(5)> 1 and true
** (BadBooleanError) expected a boolean on left-side of "and", got: 1
除了这几个布尔运算符,Elixir还提供||
,&&
和!
运算符。它们可以接受任意类型的参数值。 在使用这些运算符时,除了 false 和 nil 的值都被视作 true:
# or
iex(5)> 1 || true
1
iex(6)> false || 2
2
# and
iex(7)> nil && 11
nil
iex(8)> true && 22
22
# !
iex(9)> !true
false
iex(10)> !1
false
iex(11)> !nil
true
当参数确定是布尔时,使用and,or和not; 如果非布尔值(或不确定是不是),用&&
,||
和!
。
字符串拼接
使用 <>
操作符进行字符串拼接:
iex(17)> name = "Elixir"
"Elixir"
iex(18)> "hello " <> name
"hello Elixir"
比较运算符
Elixir 有我们习惯的一切比较运算符 :==
, !=
, ===
, !==
, <=
, >=
, < 和 >
iex(7)> 1 > 1
false
iex(8)> 1 != 1
false
iex(9)> 1 == 1
true
iex(10)> 2 <= 3
true
对于整数和浮点数的严格比较,可以使用 ===
:
iex(11)> 2 == 2.0
true
iex(12)> 2 === 2.0
false
在Elixir中,可以判断不同类型数据的大小:
iex(12)> 1 < :atom
true
这很实用。排序算法不必担心如何处理不同类型的数据。总体上,不同类型的排序顺序是:
number < atom < reference < functions < port < pid < tuple < maps < list < bitstring
字符串插值
如果你使用过 Ruby,那么 Elixir 的字符串插值看起来会很熟悉:
iex(13)> name = "Elixir"
"Elixir"
iex(14)> "I love #{name}"
"I love Elixir"
Enum板块
- Enum
- all?
- any?
- chunk_every/2
- chunk_by
- map_every
- each
- map
- min
- max
- reduce
- sort
- uniq_by
Enum
Enum 模块提供了超过一百个函数.
这节我们只会覆盖一部分函数,不过我们随时都可以自己去了解。 让我们在 IEx 里做个小试验
iex
iex> Enum.__info__(:functions) |> Enum.each(fn({function, arity}) ->
...> IO.puts "#{function}/#{arity}"
...> end)
all?/1
all?/2
any?/1
any?/2
at/2
at/3
...
通过上面的命令,很明显能看到我们有很多函数,而且这些函数都是师出有名。
集合在函数式编程中处于核心地位,同时也是非常有用的部分。
通过利用它与Elixir的其他优势相结合,比如我们刚看到的,文档是一等公民,可以赋予开发人员非常强大的能力。
要想了解全部的函数,请访问官方的 Enum 文档。而要想了解惰性枚举(lazy enumeration),访问 Stream 模块。
all?
使用all?
以及大部分Enum
函数的时候,我们要提供一个函数来作用到要操作的集合上。
只有当函数在所有的元素上都返回true
的时候,all? 才会返回true
,否则结果就是false
。
iex(13)> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 3 end)
false
iex(14)> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) > 1 end)
true
any?
和上面不同,只要有一个元素被函数调用返回true
,any?
就会返回true
。
iex(15)> Enum.any?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 5 end)
true
chunk_every/2
如果你想把你的集合拆分成小的分组,chunk_every/2
就是你要找的函数:
iex(16)> Enum.chunk_every([1, 2, 3, 4, 5, 6], 2)
[[1, 2], [3, 4], [5, 6]]
chunk_every/2 还有其他选项,在这里不深入介绍。如果感兴趣,前往 chunk_every/4 的官方文档去了解
chunk_by
如果不按照数量分组(每组的元素数量相同),我们可以使用 chunk_by 方法。
它接受一个枚举值和一个函数作为参数,如果函数的返回值变了,就是从新从后开始分组 :
iex(17)> Enum.chunk_by(["one", "two", "three", "four", "five"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"]]
iex(18)> Enum.chunk_by(["one", "two", "three", "four", "five", "six"], fn(x) -> String.length(x) end)
[["one", "two"], ["three"], ["four", "five"], ["six"]]
map_every
有时候把集合分组并不能满足我们的需求。这时候 map_every/3 在修改集合中特定元素的时候会非常有用:
iex(19)> Enum.map_every([1, 2, 3, 4], 2, fn x -> x * 2 end)
[2, 2, 6, 4]
each
有时候需要遍历某个集合进行操作,但是不想产生新的值(不把函数的遍历调用结果返回),这种情况下,可以使用each
:
iex(20)> Enum.each(["one", "two", "three"], fn(s) -> IO.puts(s) end)
one
two
three
:ok
注意:each
函数会返回原子:ok
map
要把某个元素都执行某个函数,并且把结果作为新的集合返回,要使用 map 函数:
iex(21)> Enum.map([0, 1, 2, 3], fn(x) -> x - 1 end)
[-1, 0, 1, 2]
min
min/1
在集合中找到最小的值:
iex(22)> Enum.min([5, 3, 0, -1])
-1
min/2
也一样,但是它允许我们提供一个匿名函数指定计算最小值的方法:
iex(23)> Enum.min([], fn -> :foo end)
:foo
max
max/1
返回集合中最大的值:
iex(24)> Enum.max([5, 3, 0, -1])
5
max/2
也一样,而且像min/2
一样,它允许我们提供一个匿名函数指定计算最大值的方法:
iex(25)> Enum.max([], fn -> :bar end)
:bar
reduce
使用reduce
,我们可以把集合不断计算,最终得到一个值。我们需要提供一个可选的累加值(在这个例子中是 10),
如果没有累加值,集合中的第一个值会被使用。
iex(26)> Enum.reduce([1, 2, 3], 10, fn(x, acc) -> x + acc end)
16
iex(27)> Enum.reduce([1, 2, 3], fn(x, acc) -> x + acc end)
6
iex(28)> Enum.reduce(["a","b","c"], "1", fn(x,acc)-> x <> acc end)
"cba1"
sort
对集合进行排序,Elixir 提供了两个sort
函数来帮忙。第一个使用 Elixir 默认的排序规则进行排序:
iex(29)> Enum.sort([5, 6, 1, 3, -1, 4])
[-1, 1, 3, 4, 5, 6]
iex(30)> Enum.sort([:foo, "bar", Enum, -1, 4])
[-1, 4, Enum, :foo, "bar"]
另外sort
允许我们自己提供排序函数:
# with our function
iex(31)> Enum.sort([%{:val => 4}, %{:val => 1}], fn(x, y) -> x[:val] > y[:val] end)
[%{val: 4}, %{val: 1}]
# without
iex(32)> Enum.sort([%{:count => 4}, %{:count => 1}])
[%{count: 1}, %{count: 4}]
uniq_by
我们可以使用 uniq_by/2
删除集合中的重复元素:
iex(33)> Enum.uniq_by([1, 2, 3, 2, 1, 1, 1, 1, 1], fn x -> x end)
[1, 2, 3]
这跟我们以前都熟悉的uniq/1
函数一样,但是这个函数在 Elixir 1.4 中被弃用,不过你还是可以使用它(会收到一个警告信息)。
二:模型匹配
模式匹配是 Elixir 很强大的特性,它允许我们匹配简单值、数据结构、甚至函数。
匹配运算符
在Elixir中,=
运算符实际上叫做 匹配运算符。通过这个匹配操作符,我们可以赋值和匹配值。
我们已经多次使用=
符号进行变量的赋值操作:
iex(1)> x = 1
1
iex(2)> x
1
在Elixir中,=
作为 匹配运算符。下面来学习这样的概念:
iex(3)> 1 = x
1
iex(4)> 2 = x
** (MatchError) no match of right hand side value: 1
注意1 = x
是一个合法的表达式。 由于前面的例子给x赋值为1,因此在匹配时左右相同,所以它匹配成功了。而两侧不匹配的时候,MatchError将被抛出。
变量只有在匹配操作符=
的左侧时才被赋值:
iex(4)> 1 = unknown
** (CompileError) iex:4: undefined function unknown/0
错误原因是unknown变量没有被赋过值,Elixir猜你想调用一个名叫unknown/0
的函数, 但是找不到这样的函数。
变量名在等号左边,Elixir认为是赋值表达式;变量名放在右边,Elixir认为是拿该变量的值和左边的值做匹配。
模式匹配
匹配运算符不光可以匹配简单数值,还能用来 解构 复杂的数据类型。例如,我们在元组上使用模式匹配:
iex(4)> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex(5)> a
:hello
iex(6)> b
"world"
iex(7)> c
42
在两端不匹配的情况下,模式匹配会失败。比方说,匹配的两端的元组不一样长:
iex(8)> {a, b, c} = {:hello, "world"}
** (MatchError) no match of right hand side value: {:hello, "world"}
或者两端模式有区别(比如两端数据类型不同):
iex(8)> {a, b, c} = [:hello, "world", "!"]
** (MatchError) no match of right hand side value: [:hello, "world", "!"]
利用“匹配”的这个概念,我们可以匹配特定值;或者在匹配成功时,为某些变量赋值。
下面例子中先写好了匹配的左端,它要求右端必须是个元组,且第一个元素是原子:ok
。
iex(8)> {:ok, result} = {:ok, 13}
{:ok, 13}
iex(9)> result
13
iex(10)> {:ok, result} = {:error, :oops}
** (MatchError) no match of right hand side value: {:error, :oops}
用在列表上:
iex(11)> [a, 2, 3] = [1, 2, 3]
[1, 2, 3]
iex(12)> a
1
列表支持匹配自己的head和tail (这相当于同时调用hd/1
和tl/1
,给head和tail赋值):
iex(13)> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex(14)> head
1
iex(15)> tail
[2, 3]
同hd/1
和tl/1
函数一样,以上代码不能对空列表使用:
iex(16)> [h|t] = []
** (MatchError) no match of right hand side value: []
[head|tail]
这种形式不光在模式匹配时可以用,还可以用作向列表插入前置数值:
iex(16)> list = [1, 2, 3]
[1, 2, 3]
iex(17)> [0|list]
[0, 1, 2, 3]
模式匹配使得程序员可以容易地解构数据结构(如元组和列表)。 在后面我们还会看到,它是Elixir的一个基础,对其它数据结构同样适用,比如图和二进制。
小结:
- 模式匹配使用=符号
- 匹配中等号左右的“模式”必须相同
- 变量在等号左侧才会被赋值
- 变量在右侧时必须有值,Elixir拿这个值和左侧相应位置的元素做匹配
pin运算符
当匹配的左边包含变量的时候,匹配操作符同时会做赋值操作。有些时候,这种行为并不是预期的,这种情况下,我们可以使用^
操作符。
在Elixir中,变量可以被重新绑定:
iex(18)> x = 1
1
iex(19)> x = 2
2
Elixir可以给变量重新绑定(赋值)。 它带来一个问题,就是对一个单独变量(而且是放在左端)做匹配时, Elixir会认为这是一个重新绑定(赋值)操作,而不会当成匹配,执行匹配逻辑。 这里就要用到pin运算符。
如果你不想这样,可以使用pin运算符(^)。 加上了pin运算符的变量,在匹配时使用的值是本次匹配前就赋予的值:
这个例子直接取自 Elixir的 官方指南
iex(20)> x = 1
1
iex(21)> ^x = 2
** (MatchError) no match of right hand side value: 2
iex(21)> {x, ^x} = {2, 1}
{2, 1}
iex(22)> x
2
注意如果一个变量在匹配中被引用超过一次,所有的引用都应该绑定同一个模式:
iex(23)> {x, x} = {1, 1}
{1, 1}
iex(24)> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
有些时候,你并不在意模式匹配中的一些值。 可以把它们绑定到特殊的变量 “ _ ” (underscore)上。 例如,如果你只想要某列表的head,而不要tail值。你可以这么做:
iex(24)> [h | _ ] = [1, 2, 3]
[1, 2, 3]
iex(25)> h
1
变量“ _ ”特殊之处在于它不能被读,尝试读取它会报“未绑定的变量”错误:
iex(26)> _
** (CompileError) iex:26: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions
尽管模式匹配看起来如此牛逼,但是语言还是对它的作用做了一些限制。 比如,你不能让函数调用作为模式匹配的左端。下面例子就是非法的:
iex(26)> length([1,[2],3]) = 3
** (CompileError) iex:26: cannot invoke remote function :erlang.length/1 inside match
流程控制
case
case将一个值与许多模式进行匹配,直到找到一个匹配成功的:
iex> case {1, 2, 3} do
...> {4, 5, 6} ->
...> "This clause won't match"
...> {1, x, 3} ->
...> "This clause will match and bind x to 2 in this clause"
...> _ ->
...> "This clause would match any value"
...> end
如果与一个已赋值的变量做比较,要用pin运算符(^)标记该变量:
iex> x = 1
1
iex> case 10 do
...> ^x -> "Won't match"
...> _ -> "Will match"
...> end
如果case中没有一条模式能匹配,会报错:
iex> case :ok do
...> :error -> "Won't match"
...> end
** (CaseClauseError) no case clause matching: :ok
匿名函数也可以像下面这样,用多个模式或卫兵条件来灵活地匹配该函数的参数:
iex> f = fn
...> x, y when x > 0 -> x + y
...> x, y -> x * y
...> end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> f.(1, 3)
4
iex> f.(-1, 3)
-3
需要注意的是,所有case模式中表示的参数个数必须一致,否则会报错。 上面的例子两个待匹配模式都是x,y。如果再有一个模式表示的参数是x,y,z,那就不行:
iex(5)> f2 = fn
...(5)> x,y -> x+y
...(5)> x,y,z -> x+y+z
...(5)> end
** (CompileError) iex:5: cannot mix clauses with different arities in function definition
(elixir) src/elixir_translator.erl:17: :elixir_translator.translate/2
_
变量是case
语句重要的一项,如果没有_
,所有模式都无法匹配的时候会抛出异常:
iex> case :even do
...> :odd -> "Odd"
...> end
** (CaseClauseError) no case clause matching: :even
iex> case :even do
...> :odd -> "Odd"
...> _ -> "Not Odd"
...> end
"Not Odd"
可以把 _ 想象成最后的 else,会匹配任何东西。因为 case 依赖模式匹配,所以之前所有的规则和限制在这里都适用。 如果你想匹配已经定义的变量,一定要使用 pin 操作符 ^:
iex> pie = 3.14
3.14
iex> case "cherry pie" do
...> ^pie -> "Not so tasty"
...> pie -> "I bet #{pie} is tasty"
...> end
"I bet cherry pie is tasty"
case
还有一个很酷的特性:它支持卫兵表达式:
这个例子直接取自 Elixir官方指南的上手教程
iex> case {1, 2, 3} do
...> {1, x, 3} when x > 0 ->
...> "Will match"
...> _ ->
...> "Won't match"
...> end
"Will match"
参考官方的文档来看卫兵支持的表达式
cond
case是拿一个值去同多个值或模式进行匹配,匹配了就执行那个分支的语句。 然而,许多情况下我们要检查不同的条件,找到第一个结果为true的,执行它的分支。 这时我们用cond:
iex> cond do
...> 2 + 2 == 5 ->
...> "This will not be true"
...> 2 * 2 == 3 ->
...> "Nor this"
...> 1 + 1 == 2 ->
...> "But this will"
...> end
"But this will"
这样的写法和命令式语言里的else if差不多一个意思(尽管很少这么写)。
如果没有一个条件结果为true,会报错。因此,实际应用中通常会使用true作为最后一个条件。 因为即使上面的条件没有一个是true,那么该cond表达式至少还可以执行这最后一个分支:
iex> cond do
...> 2 + 2 == 5 ->
...> "This is never true"
...> 2 * 2 == 3 ->
...> "Nor this"
...> true ->
...> "This is always true (equivalent to else)"
...> end
用法就好像许多语言中,switch语句中的default一样。
最后需要注意的是,cond视所有非false和nil的值为true:
iex> cond do
...> hd([1,2,3]) ->
...> "1 is considered as true"
...> end
"1 is considered as true"
if和unless
你之前可能遇到过if/2
了,如果你使用过 Ruby,也会很熟悉unless
。它们在 Elixir 使用方式也一样,只不过它们在 Elixir 里是宏定义,不是语言本身的语句。
你可以在 Kernel模块 找到它们的实现。
Kernel 模块还定义了诸如+/2运算符和is_function/2函数。 它们默认被导入,因而在你的代码中可用。
需要注意的是,Elixir 中唯一为假的值是nil
和布尔值false
。
iex> if String.valid?("Hello") do
...> "Valid string!"
...> else
...> "Invalid string."
...> end
"Valid string!"
iex> if "a string value" do
...> "Truthy"
...> end
"Truthy"
unless/2
使用方法和if/2
一样,不过只有当判断为否定才会继续执行:
iex> unless is_integer("hello") do
...> "Not an Int"
...> end
"Not an Int"
它们都支持else语句块:
iex> if nil do
...> "This won't be seen"
...> else
...> "This will"
...> end
"This will"
do语句块
以上讲解的4种流程控制结构:case,cond,if和unless,它们都被包裹在do/end语句块中。 即使我们把if语句写成这样:
iex> if true, do: 1 + 2
3
在Elixir中,do/end
语句块方便地将一组表达式传递给do:
。以下是等同的:
iex> if true do
...> a = 1 + 2
...> a + 10
...> end
13
iex> if true, do: (
...> a = 1 + 2
...> a + 10
...> )
13
我们称第二种语法使用了 关键字列表(keyword lists)。我们可以这样传递else:
iex> if false, do: :this, else: :that
:that
注意一点,do/end
语句块永远是被绑定在最外层的函数调用上。例如:
iex> is_number if true do
...> 1 + 2
...> end
将被解析为:
iex> is_number(if true) do
...> 1 + 2
...> end
这使得Elixir认为你是要调用函数is_number/2(第一个参数是if true,第二个是语句块)。 这时就需要加上括号解决二义性:
iex> is_number(if true do
...> 1 + 2
...> end)
true
关键字列表在Elixir语言中占有重要地位,在许多函数和宏中都有使用。
函数
Elixir 和其他函数式语言一样,函数都是一等公民。
Elixir中有不同类型的函数,让我们来看看它们与众不同的地方,以及如何来使用它们。
匿名函数
就像名字中说明的那样,匿名函数没有名字。我们在Enum
课程中看到过,它们经常被用来传递给其他函数。
要定义匿名函数,我们需要fn
和end
关键字,在这两者之间,我们可以定义任意数量的参数和函数体,它们用->
分隔开。
我们来看一个简单的例子:
iex(1)> sum = fn (a, b) -> a + b end
#Function<12.99386804/2 in :erl_eval.expr/5>
iex(2)> sum.(2, 3)
5
)& 操作符
因为在 Elixir 中使用匿名函数非常常见,所以有一个快捷方式来做这件事:
iex(4)> sum = &(&1 + &2)
&:erlang.+/2
iex(5)> sum.(2, 3)
5
你可能也猜到了,在这种简写的模式下,函数的参数可以通过&1
,&2
,&3
等来获取。
模式匹配
在 Elixir 中模式匹配不仅限于变量,也可以用在函数签名上。
Elixir 使用模式匹配来找到第一个匹配参数的模式,然后执行它后面的函数体。
iex> handle_result = fn
...> {:ok, result} -> IO.puts "Handling result..."
...> {:error} -> IO.puts "An error has occurred!"
...> end
iex> some_result = 1
iex> handle_result.({:ok, some_result})
Handling result...
iex> handle_result.({:error})
An error has occurred!
命名函数
我们也可以定义有名字的函数,这样在后面可以直接用名字来使用它。
命名函数通过def
关键字定义在某个模块中。
定义在模块内部的函数可以被其他模块使用,这在 Elixir 中构建代码块非常有用:
defmodule Greeter do
def hello(name) do
"Hello, " <> name
end
end
iex> Greeter.hello("Sean")
"Hello, Sean"
如果我们的函数体只有一行,我们可以缩写成do::
defmodule Greeter do
def hello(name), do: "Hello, " <> name
end
学到了那么多模式匹配的知识,现在我们用命名函数实现递归:
defmodule Length do
def of([]), do: 0
def of([_ | tail]), do: 1 + of(tail)
end
iex> Length.of []
0
iex> Length.of [1, 2, 3]
3
函数名字和元数
我们之前提到过,函数名称方式由名字和元数组成,这也表明你可以这么做:
defmodule Greeter2 do
def hello(), do: "Hello, anonymous person!" # hello/0
def hello(name), do: "Hello, " <> name # hello/1
def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
# hello/2
end
iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"
我们在上面代码注释中列出了函数的全称。
第一个函数不接受任何参数,因此是 hello/0
;第二个函数接受一个参数,因此是 hello/1
,以此类推。
不同于其他语言的函数重载,这些函数被认为是不同的。(刚刚提到过的模式匹配,只有当函数名字和接受的参数个数都匹配的时候才成立。)
私有函数
如果我们不想其他模块使用某个函数,我们可以使用私有函数,也就是只能被它所在模块调用的函数。在 Elixir 中,我们可以用 defp
来定义私有函数:
defmodule Greeter do
def hello(name), do: phrase <> name
defp phrase, do: "Hello, "
end
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
Greeter.phrase()
卫兵
我们在控制语句那一个提过卫兵,现在我们就来看看怎么在命名函数中使用它们。
当 Elixir 匹配某个函数之后,后面的卫兵都会被检测。
在下面的例子中,我们定义了两个有相同签名的函数,而依赖判断参数类型的卫兵来确定调用哪个函数:
defmodule Greeter do
def hello(names) when is_list(names) do
names
|> Enum.join(", ")
|> hello
end
def hello(name) when is_binary(name) do
phrase() <> name
end
defp phrase, do: "Hello, "
end
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
参数默认值
如果想给参数设置默认值,我们可以用argument \\ value
语法:
defmodule Greeter do
def hello(name, language_code \\ "en") do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
iex> Greeter.hello("Sean", "en")
"Hello, Sean"
iex> Greeter.hello("Sean")
"Hello, Sean"
iex> Greeter.hello("Sean", "es")
"Hola, Sean"
当我们同时使用卫兵和默认参数值的时候,会遇到问题,先看一下程序会报什么错:
defmodule Greeter do
def hello(names, language_code \\ "en") when is_list(names) do
names
|> Enum.join(", ")
|> hello(language_code)
end
def hello(name, language_code \\ "en") when is_binary(name) do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
** (CompileError) iex:31: definitions with multiple clauses and default values require a header. Instead of:
def foo(:first_clause, b \\ :default) do ... end
def foo(:second_clause, b) do ... end
one should write:
def foo(a, b \\ :default)
def foo(:first_clause, b) do ... end
def foo(:second_clause, b) do ... end
def hello/2 has multiple clauses and defines defaults in one or more clauses
iex:31: (module)
Elixir在处理多个匹配函数的时候,不喜欢默认参数这种模式,因为它很容易让人混淆。要处理这种情况,我们可以添加一个设置了默认参数值的函数头部:
defmodule Greeter do
def hello(names, language_code \\ "en")
def hello(names, language_code) when is_list(names) do
names
|> Enum.join(", ")
|> hello(language_code)
end
def hello(name, language_code) when is_binary(name) do
phrase(language_code) <> name
end
defp phrase("en"), do: "Hello, "
defp phrase("es"), do: "Hola, "
end
iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"
iex> Greeter.hello ["Sean", "Steve"], "es"
"Hola, Sean, Steve"
管道操作符
管道操作符|>
把前面表达式的结果传递给后面的表达式作为第一个参数。
简介
编程可以变得很混乱,比如函数调用有多层嵌套以至于很难阅读,看下面这个例子:
foo(bar(baz(new_function(other_function()))))
这个例子中,我们把other_function/0
的值传递给new_function/1
,
把new_function/1
的值传递给baz/1
,把baz/1
的值传递给bar/1
,
最后把bar/1
的结果传递给foo/1
。Elixir 给我们提供了管道操作符来解决这个语法上的混乱。
管道操作符|>
获取一个表达式的结果,并把它往后传递。 我们把上面的代码用管道重写看看:
other_function() |> new_function() |> baz() |> bar() |> foo()
管道获取左边的值,并把它传递给右边。
示例
对于下面的例子,我们会用到 Elixir 的 String 模块:
字符分组
iex> "Elixir rocks" |> String.split()
["Elixir", "rocks"]
把所有分组大写
iex> "Elixir rocks" |> String.upcase() |> String.split()
["ELIXIR", "ROCKS"]
检查尾部字符串
iex> "elixir" |> String.ends_with?("ixir")
true
最佳实践
如果函数的元数大于 1,一定要使用括号。
尽管这个语法对 Elixir 并没有实质上的影响,但是可能会让其他程序员错误理解你的代码。
如果我们上面第三个例子中String.ends_with?/2
的括号去掉,会收到下面的警告:
iex> "elixir" |> String.ends_with? "ixir"
warning: parentheses are required when piping into a function call. For example:
foo 1 |> bar 2 |> baz 3
is ambiguous and should be written as
foo(1) |> bar(2) |> baz(3)
true
二进制串、字符串和字符列表
在“基本类型”一章中,介绍了字符串,以及使用is_binary/1
函数检查它:
iex> string = "hello"
"hello"
iex> is_binary string
true
接下来学习和理解:二进制串(binaries)是个啥,它怎么和字符串(strings)扯上关系的。
以及用单引号包裹的值'like this'是啥意思。
UTF-8和Unicode
字符串是UTF-8编码的二进制串。
为了弄清这句话的准确含义,我们要先理解两个概念:字节(bytes)和字符编码(code point)的区别。
Unicode标准为我们已知的大部分字母分配了字符编码。
比如,字母a
的字符编码是97
,而字母ł
的字符编码是322
。
当把字符串"hełło"
写到硬盘上的时候,需要将字符编码转化为字节。
如果我们遵循一个字节表示一个字符编码这个,那是写不了"hełło"
的。
因为字母ł
的编码是322
,而一个字节所能存储的数值范围是0
到255
。
但是如你所见,确实能够在屏幕上显示"hełło"
,说明还是有某种解决方法的,于是编码便出现了。
要用字节表示字符编码,我们需要用某种方式对其进行编码。
Elixir选择UTF-8为主要并且默认的编码方式。
当我们说某个字符串是UTF-8编码的二进制串,指的是该字符串是一串字节,
这些字节以某种方式(即UTF-8编码)组织起来,表示特定的字符编码。
因为给字母ł分配的字符编码是322
,因此在实际上需要一个以上的字节来表示。
这就是为什么我们会看到,调用函数byte_size/1
和String.length/1
的结果不一样:
iex(1)> string = "hełło"
"hełło"
iex(2)> byte_size string
7
iex(3)> String.length string
5
注意:如果你使用Windows,你的终端有可能不是默认使用UTF-8编码方式。
你需要在进入iex(iex.bat)
之前, 首先执行chcp 65001
命令来修改当前Session的编码方式。
UTF-8需要1个字节来表示h
,e
,o
的字符编码,用2个字节表示ł
。 在Elixir中可以使用?
运算符获取字符的编码:
iex(4)> ?a
97
iex(5)> ?ł
322
你还可以使用 String模块里的函数,将字符串切成单独的字符编码:
iex(6)> String.codepoints("hełło")
["h", "e", "ł", "ł", "o"]
Elixir为字符串操作提供了强大的支持,它支持Unicode的许多操作。实际上,Elixir通过了文章 “字符串类型崩坏了” 记录的所有测试。
然而,字符串只是故事的一小部分。如果字符串正如所言是二进制串,那我们使用is_binaries/1
函数时, Elixir必须一个底层类型来支持字符串。事实亦如此,下面就来介绍这个底层类型---二进制串。
然而,字符串只是故事的一小部分。如果字符串正如所言是二进制串,那我们使用is_binaries/1
函数时, Elixir必须一个底层类型来支持字符串。事实亦如此,下面就来介绍这个底层类型---二进制串。
二进制串(以及比特串bitstring)
在Elixir中可以用<<>>
定义一个二进制串:
iex(7)> <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex(8)> byte_size(<<0, 1, 2, 3>>)
4
一个二进制串只是一连串的字节而已。这些字节可以以任何方式组织,即使凑不成一个合法的字符串:
iex(9)> String.valid?(<<239, 191, 191>>)
true
而字符串的拼接操作实际上就是二进制串的拼接操作:
iex(10)> <<0, 1>> <> <<2, 3>>
<<0, 1, 2, 3>>
一个常见技巧是,通过给一个字符串尾部拼接一个空(null)字节<<0>>
, 可以看到该字符串内部二进制串的样子:
"hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>
二进制串中的每个数值都表示一个字节,其数值最大范围是255。 二进制允许使用修改器显式标注一下那个数值的存储空间大小,使其可以存储超过255的数值; 或者将一个字符编码转换为utf8编码后的形式(变成多个字节的二进制串):
iex> <<255>>
<<255>>
iex> <<256>> # 被截断(truncated)
<<0>>
iex> <<256 :: size(16)>> # 使用16比特(bits)即2个字节来保存
<<1, 0>>
iex> <<256 :: utf8>> # 这个数字是一个字符的编码,将其使用utf8方式编码为字节
"Ā" # 注意,在iex交互窗口中,所有可以作为合法字符串的二进制串,都会显示为字符串
iex> <<256 :: utf8, 0>> # 尾部拼接个空字节,查看上一条命令结果内部实际的二进制串
<<196, 128, 0>>
如果一个字节是8个比特,那如果我们给一个大小是1比特的修改器会怎样?:
iex> <<1 :: size(1)>>
<<1::size(1)>>
iex> <<2 :: size(1)>> # 被截断(truncated)
<<0::size(1)>>
iex> is_binary(<< 1 :: size(1)>>) # 二进制串失格
false
iex> is_bitstring(<< 1 :: size(1)>>)
true
iex> bit_size(<< 1 :: size(1)>>)
1
这样(每个元素长度是1比特)就不再是二进制串(人家每个元素是一个字节,起码8比特), 退化成为比特串(bitstring),意思就是一串比特! 所以,所以,二进制串就是一特殊的比特串,比特总数是8的倍数。
也可以对二进制串或比特串做模式匹配:
iex> <<0, 1, x>> = <<0, 1, 2>>
<<0, 1, 2>>
iex> x
2
iex> <<0, 1, x>> = <<0, 1, 2, 3>>
** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>
注意,在没有修改器标识的情况下,二进制串中的每个元素都应该匹配8个比特长度。 因此上面最后的例子,匹配的左右两端不具有相同容量,因此出现错误。
下面是使用了修改器标识的匹配例子:
iex> <<0, 1, x :: binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> x
<<2, 3>>
上面例子使用了binary修改器,指示x是个二进制串。(为啥不用单词的复数形式binaries搞不懂啊。) 这个修改器仅仅可以用在被匹配的串的末尾元素上。
跟上面例子同样的原理,使用字符串的连接操作符<>,效果相似:
iex> "he" <> rest = "hello"
"hello"
iex> rest
"llo"
关于二进制串/
比特串的构造器<< >>
完整的参考, 请见 Elixir的文档。
总之,记住字符串是UTF-8编码后的二进制串,而二进制串是特殊的、元素数量是8的倍数的比特串。 尽管这种机制增加了Elixir在处理比特或字节时的灵活性, 而现实中99%的时候你只会用到is_binary/1和byte_size/1函数跟二进制串打交道。
字符列表(char lists)
字符列表就是字符的列表。
iex> 'hełło'
[104, 101, 322, 322, 111]
iex> is_list 'hełło'
true
iex> 'hello'
'hello'
可以看出,比起包含字节,一个字符列表包含的是单引号所引用的一串字符各自的字符编码。 注意IEx遇到超出ASCII值范围的字符编码时,显示其字符编码的值,而不是字符。 双引号引用的是字符串(即二进制串),单引号表示的是字符列表(即,一个列表)。
实际应用中,字符列表常被用来做为同一些Erlang库交互的参数,因为这些老库不接受二进制串作为参数。 要将字符列表和字符串之间相互转换,可以使用函数to_string/1和to_char_list/1:
iex> to_char_list "hełło"
[104, 101, 322, 322, 111]
iex> to_string 'hełło'
"hełło"
iex> to_string :hello
"hello"
iex> to_string 1
"1"
注意这些函数是多态的。它们不但可以将字符列表转化为字符串,还能转化整数、原子等为字符串。