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的条件控制方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容