Elixir 简明笔记(十五) --- 控制结构之模式匹配

编程语言中,流程控制是重要的一部分。流程大致可以分为顺序条件循环结构。有趣的是Elixir并没有直接提供这些结构的关键字,而是通过模式匹配,枚举迭代,递归来实现流程控制。

模式匹配

前面介绍了简单的模式匹配,尤其是介绍不同的数据结构时候,也针对该模块提供模式匹配的方式。本篇关于模式匹配的讨论,更像是模式匹配的总结。

基本定义

所谓模式匹配,即使用匹配符=将左边和右边的变量进行绑定。左边的表达式或变量称之为:模式(pattern),右边的表达式进行求值。然后将右边求值的结果与左边的模式进行匹配,匹配成功则绑定对应的变量。模式匹配的表达式返回右边表达式的求值结果。

Tuple匹配

模式可以是一个变量,也可以是一个元组。elixir中的所有表达式都会返回值,模式匹配中将会把匹配成功的右边求值返回:

iex(1)> person = {"Bob", 25}            # 匹配成功,返回右边表达式的求值结果,元组本身的求值返回元组
{"Bob", 25}
iex(2)> {name, age} = {"Bob", 25}
{"Bob", 25}
iex(3)> name                            # 绑定变量 name
"Bob"
iex(4)> age                             # 绑定变量 age
25

Elixir中,经常把函数返回的多个值放到tuple当中,常用与tuple的pattern match。并且还可以嵌套匹配:

iex(1)> {date, time} = :calendar.local_time
{{2016, 4, 7}, {20, 49, 11}}
iex(2)> {year, month, day} = date
{2016, 4, 7}
iex(3)> year
2016
iex(4)> {{year, month, day}, {hour, minutes, second}} = :calendar.local_time
{{2016, 4, 7}, {20, 58, 3}}
iex(5)> year
2016
iex(6)> hour
20
iex(7)> {{year, month, day}, {hour, minutes, second}} = {date, time} = :calendar.local_time
{{2016, 4, 7}, {20, 59, 7}}
iex(8)> month
4
iex(9)> date
{2016, 4, 7}

最后一个表达式就是嵌套匹配,即先从最右边的进行模式匹配,然后把最右边的表达式求值作为模式匹配成功的结果(:calendar.local_time)返回,然后这个结果继续和左边的{{year, month, day}, {hour, minutes, second}}进行模式匹配。

固定匹配

尽管elixir的数据是不变的,可是变量却可以重新绑定。有时候并不需要变量被重新绑定,此时可以使用pin 操作符 (^)来固定匹配。

iex(10)> expected_name = "Bob"
"Bob"
iex(11)> {^expected_name, _} = {"Bob", 25}     # 此时固定了expected_name
{"Bob", 25}
iex(12)> {^expected_name, _} = {"Alice", 30}      
** (MatchError) no match of right hand side value: {"Alice", 30}

List匹配

匹配列表和匹配元组的差别不是很大。由于列表操作head和tail的特殊性,因此可以使用|来匹配列表,当遇到不想匹配绑定的变量,可以使用_,表示可以匹配任何模式,并且不绑定变量:

iex(15)> [1, second, third] = [1, 2, 3]
[1, 2, 3]
iex(16)> second
2
iex(17)> [head|tail] = [1, 2, 3]
[1, 2, 3]
iex(18)> head
1
iex(19)> [1|tail] = [1, 2, 3]
[1, 2, 3]
iex(20)> tail
[2, 3]

匹配其实很灵活,同一个变量可以匹配多次,但不能匹配多个变量:

iex(21)> [first, first, first] = [1, 1, 1]
[1, 1, 1]
iex(22)> first
1
iex(23)> [^first, second, _] = [1, 2, 3]
[1, 2, 3]
iex(24)> first
1
iex(25)> second
2
iex(26)> [first|first] = [1, 1]
** (MatchError) no match of right hand side value: [1, 1]

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

Map匹配

map的匹配和列表与元组都不一样,list和tuple都必须把需要匹配的元素都写出来,list的|也是。而map可以只匹配部分模式, 匹配失败则会报错:

iex(26)> %{age: age} = %{name: "Bob", age: 25}
%{age: 25, name: "Bob"}
iex(27)> age
25
iex(28)> name
** (RuntimeError) undefined function: name/0
iex(28)> %{age: age, work_at: work_at} = %{name: "Bob", age: 25}
** (MatchError) no match of right hand side value: %{age: 25, name: "Bob"}

Function 匹配

函数的参数可以进行模式匹配。同一个函数名,不同的参数模式可以匹配不同的参数,执行多路函数逻辑,匹配失败则会抛出异常:

iex(1)> defmodule Geometry do
...(1)>   def area({:rectangle, a, b}) do
...(1)>     a * b
...(1)>   end
...(1)>
...(1)>   def area({:square, a}) do
...(1)>     a * a
...(1)>   end
...(1)>
...(1)>   def area({:circle, r}) do
...(1)>     r * r * 3.14
...(1)>   end
...(1)> end
{:module, Geometry,
 <<70, 79, 82, 49, 0, 0, 5, 124, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 117, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 2, 104, 2, ...>>,
 {:area, 1}}
iex(2)> Geometry.area({:rectangle, 4, 5})
20
iex(3)> Geometry.area({:square, 5})
25
iex(4)> Geometry.area({:circle, 4})
50.24
iex(5)> Geometry.area({:triangle, 1, 2, 3})
** (FunctionClauseError) no function clause matching in Geometry.area/1
    iex:2: Geometry.area({:triangle, 1, 2, 3})

为了避免发生错误,可以写一个处理错误的匹配函数,通过万能匹配handler错误。需要注意,定义万能匹配不能放到模块的第一个函数,因为函数是按照顺序从上向下依次匹配的,如果写在第一个,则用于无法匹配后面的逻辑函数:

iex(1)> defmodule Geometry do
...(1)>   def area({:rectangle, a, b}) do
...(1)>     a * b
...(1)>   end
...(1)>
...(1)>   def area({:square, a}) do
...(1)>     a * a
...(1)>   end
...(1)>
...(1)>   def area({:circle, r}) do
...(1)>     r * r * 3.14
...(1)>   end
...(1)>   def area(unknow) do
...(1)>     {:error, {:unknow_shape, unknow}}
...(1)>   end
...(1)> end

...(2)>Geometry.area({:triangle, 1, 2, 3})
{:error, {:unknown_shape, {:triangle, 1, 2, 3}}}

匿名函数中,我们使用&来引用函数,函数也可以写这样的语法糖来匹配:

iex(5)> fun = &Geometry.area/1
&Geometry.area/1
iex(6)> fun.({:circle, 4})
50.24
iex(7)> fun.({:square, 5})
25

Guards 卫子句

除了参数进行模式匹配,函数还可以提供guards语句。通过guard语句过滤一参数。

iex(8)> defmodule TestNum do
...(8)>   def test(x) when x < 0 do
...(8)>     :negative
...(8)>   end
...(8)>
...(8)>   def test(0) , do: :zero
...(8)>
...(8)>   def test(x) when x > 0 do
...(8)>
...(8)>     :positive
...(8)>   end
...(8)>
...(8)> end
{:module, TestNum,
 <<70, 79, 82, 49, 0, 0, 4, 184, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 111, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 2, 104, 2, ...>>,
 {:test, 1}}
iex(9)> TestNum.test(-1)
:negative
iex(10)> TestNum.test(0)
:zero
iex(11)> TestNum.test(1)
:positive
iex(12)> TestNum.test(:not_a_number)
:positive

最后一个匹配也返回了值。在Elixir中,数据类型都是可以通过比较符><进行操作的,其优先级如下:

number < atom < reference < fun < port < pid < tuple < map < list < bitstring (binary)

为了过滤非数字,可以修改guards如下:

iex(15)> defmodule TestNum do
...(15)>   def test(x) when is_number(x) and x < 0 do
...(15)>     :negative
...(15)>   end
...(15)>   def test(0), do: :zero
...(15)>   def test(x) when is_number(x) and x > 0 do
...(15)>     :positive
...(15)> end end
iex:15: warning: redefining module TestNum
{:module, TestNum,
 <<70, 79, 82, 49, 0, 0, 4, 248, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 111, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 2, 104, 2, ...>>,
 {:test, 1}}
iex(16)> TestNum.test(-1)
:negative
iex(17)> TestNum.test(:not_a_number)
** (FunctionClauseError) no function clause matching in TestNum.test/1
    iex:16: TestNum.test(:not_a_number)

写guards语句的时候需要注意,调用一下函数会引发错误。可是写在guard语句之后,错误会被隐藏,并不会抛出,gurad语句返回false。例如 length/1 函数只对list求其长度。

iex(25)> defmodule ListHelper do
...(25)>   def smallest(list) when length(list) > 0 do
...(25)>     Enum.min(list)
...(25)>   end
...(25)>   def smallest(_), do: {:error, :invalid_argument}
...(25)> end
iex:25: warning: redefining module ListHelper
{:module, ListHelper,
 <<70, 79, 82, 49, 0, 0, 5, 56, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 118, 131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115, 95, 118, 49, 108, 0, 0, 0, 2, 104, 2, ...>>,
 {:smallest, 1}}
iex(26)> length [1, 2, 3]
3
iex(27)> length {1, 2, 3}               # 对tuple求值错误
** (ArgumentError) argument error
    :erlang.length({1, 2, 3})
iex(27)> length 123                     # 对数字求值错误
** (ArgumentError) argument error
    :erlang.length(123)
iex(27)> ListHelper.smallest([1, 2, 3])
1
iex(28)> ListHelper.smallest(123)       # 没有抛出错误,匹配错误
{:error, :invalid_argument}
iex(29)> ListHelper.smallest({1, 2, 3}) # 没有抛出错误,匹配错误
{:error, :invalid_argument}
iex(30)> ListHelper.smallest(1, 2, 3)       
** (UndefinedFunctionError) undefined function: ListHelper.smallest/3
    ListHelper.smallest(1, 2, 3)

最后一个例子很有意思,尽管函数参数可以进行模式匹配,但是都是指参数签名一样的函数。最后一个例子错误,并且没有匹配错误,ListHelper.smallest/1 表示一个参数, ListHelper.smallest/3表示三个参数。模块只定义了ListHelper.smallest/1 的错误匹配,ListHelper.smallest/3则没有,所以匹配失败抛出了错误。

lambdas 匹配

命名函数可以通过定义多个函数签名来使用多模式匹配,匿名函数则不能写多个def定义,但是也可以使用多路参数进行模式匹配,写法也比较简单,并且也支持guards语句。

基本的形式为:

fn
  pattern_1 -> 
    ...             Executed if pattern_1 matches 
    ... 
  end

  pattern_2 -> 
    ...             Executed if pattern_2 matches
    ... 
  end
iex(30)> test_num = fn
...(30)>   x when is_number(x) and x < 0 ->
...(30)>     :negative
...(30)>   0 -> :zero
...(30)>   x when is_number(x) and x > 0 ->
...(30)>     :positive
...(30)> end
#Function<6.90072148/1 in :erl_eval.expr/5>
iex(31)> test_num.(-1)

通过模式匹配,可以实现很多控制结构。配合函数的guard子句,甚至都不需要if条件语句。当然Elixir确实没有if条件语句,但是提供了if宏。其作用类似if条件语句,在深入宏之前,姑且当成一回事。下一节将会介绍Elixir的条件控制方式。

你可能感兴趣的:(Elixir 简明笔记(十五) --- 控制结构之模式匹配)