Elixir学习笔记(模型匹配、控制语句)
模型匹配
模式匹配是 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语言中占有重要地位,在许多函数和宏中都有使用。