美文网首页Elixir 编程Elixir & PhoenixElixir
Elixir 简明笔记(十五) --- 控制结构之模式匹配

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

作者: 人世间 | 来源:发表于2016-04-09 14:34 被阅读505次

    编程语言中,流程控制是重要的一部分。流程大致可以分为顺序条件循环结构。有趣的是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 简明笔记(十五) --- 控制结构之模式匹配

        本文链接:https://www.haomeiwen.com/subject/rfmvlttx.html