Elixir 簡明筆記(十五) --- 控制結(jié)構(gòu)之模式匹配

編程語言中,流程控制是重要的一部分缴饭。流程大致可以分為順序暑劝、條件循環(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的條件控制方式痛垛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末草慧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匙头,更是在濱河造成了極大的恐慌漫谷,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹂析,死亡現(xiàn)場離奇詭異舔示,居然都是意外死亡,警方通過查閱死者的電腦和手機电抚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門惕稻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蝙叛,你說我怎么就攤上這事俺祠。” “怎么了借帘?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵蜘渣,是天一觀的道長。 經(jīng)常有香客問我肺然,道長蔫缸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任际起,我火速辦了婚禮拾碌,結(jié)果婚禮上吐葱,老公的妹妹穿的比我還像新娘。我一直安慰自己校翔,他們只是感情好唇撬,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著展融,像睡著了一般窖认。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上告希,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天扑浸,我揣著相機與錄音,去河邊找鬼燕偶。 笑死喝噪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的指么。 我是一名探鬼主播酝惧,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伯诬!你這毒婦竟也來了晚唇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤盗似,失蹤者是張志新(化名)和其女友劉穎哩陕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赫舒,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡悍及,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了接癌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片心赶。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖缺猛,靈堂內(nèi)的尸體忽然破棺而出缨叫,到底是詐尸還是另有隱情,我是刑警寧澤枯夜,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布弯汰,位于F島的核電站艰山,受9級特大地震影響湖雹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜曙搬,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一摔吏、第九天 我趴在偏房一處隱蔽的房頂上張望鸽嫂。 院中可真熱鬧,春花似錦征讲、人聲如沸据某。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽癣籽。三九已至,卻和暖如春滤祖,著一層夾襖步出監(jiān)牢的瞬間筷狼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工匠童, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留埂材,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓汤求,卻偏偏與公主長得像俏险,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子扬绪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容