編程語言中,流程控制是重要的一部分缴饭。流程大致可以分為順序暑劝、條件和循環(huán)結(jié)構(gòu)。有趣的是Elixir
并沒有直接提供這些結(jié)構(gòu)的關(guān)鍵字颗搂,而是通過模式匹配担猛,枚舉迭代,遞歸來實現(xiàn)流程控制。
模式匹配
前面介紹了簡單的模式匹配傅联,尤其是介紹不同的數(shù)據(jù)結(jié)構(gòu)時候先改,也針對該模塊提供模式匹配的方式。本篇關(guān)于模式匹配的討論蒸走,更像是模式匹配的總結(jié)仇奶。
基本定義
所謂模式匹配,即使用匹配符=
將左邊和右邊的變量進行綁定载碌。左邊的表達式或變量稱之為:模式(pattern)猜嘱,右邊的表達式進行求值。然后將右邊求值的結(jié)果與左邊的模式進行匹配嫁艇,匹配成功則綁定對應(yīng)的變量朗伶。模式匹配的表達式返回右邊表達式的求值結(jié)果。
Tuple匹配
模式可以是一個變量步咪,也可以是一個元組论皆。elixir中的所有表達式都會返回值,模式匹配中將會把匹配成功的右邊求值返回:
iex(1)> person = {"Bob", 25} # 匹配成功猾漫,返回右邊表達式的求值結(jié)果点晴,元組本身的求值返回元組
{"Bob", 25}
iex(2)> {name, age} = {"Bob", 25}
{"Bob", 25}
iex(3)> name # 綁定變量 name
"Bob"
iex(4)> age # 綁定變量 age
25
Elixir中,經(jīng)常把函數(shù)返回的多個值放到tuple當(dāng)中悯周,常用與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}
最后一個表達式就是嵌套匹配,即先從最右邊的進行模式匹配禽翼,然后把最右邊的表達式求值作為模式匹配成功的結(jié)果(:calendar.local_time)返回屠橄,然后這個結(jié)果繼續(xù)和左邊的{{year, month, day}, {hour, minutes, second}}進行模式匹配。
固定匹配
盡管elixir的數(shù)據(jù)是不變的闰挡,可是變量卻可以重新綁定锐墙。有時候并不需要變量被重新綁定,此時可以使用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的特殊性,因此可以使用|
來匹配列表夺脾,當(dāng)遇到不想匹配綁定的變量之拨,可以使用_
,表示可以匹配任何模式咧叭,并且不綁定變量:
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 匹配
函數(shù)的參數(shù)可以進行模式匹配听想。同一個函數(shù)名腥刹,不同的參數(shù)模式可以匹配不同的參數(shù),執(zhí)行多路函數(shù)邏輯汉买,匹配失敗則會拋出異常:
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})
為了避免發(fā)生錯誤衔峰,可以寫一個處理錯誤的匹配函數(shù),通過萬能匹配handler錯誤蛙粘。需要注意垫卤,定義萬能匹配不能放到模塊的第一個函數(shù),因為函數(shù)是按照順序從上向下依次匹配的出牧,如果寫在第一個穴肘,則用于無法匹配后面的邏輯函數(shù):
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}}}
匿名函數(shù)中,我們使用&
來引用函數(shù)舔痕,函數(shù)也可以寫這樣的語法糖來匹配:
iex(5)> fun = &Geometry.area/1
&Geometry.area/1
iex(6)> fun.({:circle, 4})
50.24
iex(7)> fun.({:square, 5})
25
Guards 衛(wèi)子句
除了參數(shù)進行模式匹配评抚,函數(shù)還可以提供guards語句。通過guard語句過濾一參數(shù)伯复。
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中,數(shù)據(jù)類型都是可以通過比較符>
和<
進行操作的啸如,其優(yōu)先級如下:
number < atom < reference < fun < port < pid < tuple < map < list < bitstring (binary)
為了過濾非數(shù)字侍匙,可以修改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語句的時候需要注意,調(diào)用一下函數(shù)會引發(fā)錯誤叮雳∠氚担可是寫在guard語句之后,錯誤會被隱藏债鸡,并不會拋出江滨,gurad語句返回false铛纬。例如 length/1 函數(shù)只對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 # 對數(shù)字求值錯誤
** (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)
最后一個例子很有意思棺弊,盡管函數(shù)參數(shù)可以進行模式匹配,但是都是指參數(shù)簽名一樣的函數(shù)擒悬。最后一個例子錯誤模她,并且沒有匹配錯誤,ListHelper.smallest/1 表示一個參數(shù)懂牧, ListHelper.smallest/3表示三個參數(shù)侈净。模塊只定義了ListHelper.smallest/1 的錯誤匹配尊勿,ListHelper.smallest/3則沒有,所以匹配失敗拋出了錯誤畜侦。
lambdas 匹配
命名函數(shù)可以通過定義多個函數(shù)簽名來使用多模式匹配元扔,匿名函數(shù)則不能寫多個def定義,但是也可以使用多路參數(shù)進行模式匹配旋膳,寫法也比較簡單澎语,并且也支持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)
通過模式匹配验懊,可以實現(xiàn)很多控制結(jié)構(gòu)擅羞。配合函數(shù)的guard子句,甚至都不需要if條件語句义图。當(dāng)然Elixir確實沒有if條件語句减俏,但是提供了if宏。其作用類似if條件語句歌溉,在深入宏之前垄懂,姑且當(dāng)成一回事。下一節(jié)將會介紹Elixir的條件控制方式痛垛。