你的程式碼聞起來很臭嗎?不用擔心!大夫幫你...越治越臭

Pattern Matching(Elixir從零開始系列 06)(鼠年全馬鐵人挑戰 W07)

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 7 唷

這是 Elixir 從零開始 系列 06 唷

前言

Pattern matching 又稱模式比對,是 Elixir 內很重要的一個環節也是一個特色,很多地方都會使用到,強烈建議必讀必看喔

The match operator

如果是在一般的程式語言中,比如說 C# 或 Java 中,= 在這段程式碼內的意思就是

把 22 指派給變數 val

1
int val = 22;

但是當我們換到 Elixir 中,就沒有所謂指派給,這種運算子,打個比方,在之前的文章中常常會看到以下類似的程式碼

1
iex(1)> person = {"Bob", 25}

在這裡,在等號左邊的 person 我們稱呼它為 pattern,在等號右邊的 {"Bob", 25} 則可以稱呼為 termperson = {"Bob", 25} 這整句話的意思呢,我們可以這樣說

We match the variable pseron to the right-side term {"Bob", 25}

或者我們可這樣想

想像成拼圖,當等號左邊(pseron)與等號右邊({"Bob", 25})長得很像時,就可以合理推測兩邊的外型是一樣的(同樣都是 tuple ),同時間,外型一樣就代表內容也是一樣的,內容一樣就等於 pseron 的內容就是 {"Bob", 25}

以上就一個模式比對的解釋,可能聽不懂?沒關係!讓我們來舉更多更多的例子來解釋

Fail matching

模式比對都是要比對成功的,那如果是一個失敗的比對呢?這左右兩邊的外型長的根本不一樣,自然也就匹配不起來囉

1
2
iex(4)> {name, age} = "hello world"
** (MatchError) no match of right hand side value: "hello world"

再一發

1
2
3
4
iex(5)> {amount, amount, amount} = {127, 127, 127}
{127, 127, 127}
iex(6)> {amount, amount, amount} = {127, 127, 1}
** (MatchError) no match of right hand side value: {127, 127, 1}

這裡的 amount 被匹配為 127,當下一行再次做比對的時候會發現失敗,因為左邊的 pattern 形狀是必須三個變數都長得一樣才行,有一個 127 不等於 1 就不行啦

Matching tuples

更多的例子來了

1
iex(1)> {name, age} = {"Bob", 25}

利用模式比對來比較的話,name 的值為 "Bob"age 的值為 25

再來一個

1
2
3
4
5
6
iex(1)> {_, time} = :calendar.local_time()
iex(2)> time
{20, 44, 18}
iex(3)> {_, {hour, _, _}} = :calendar.local_time()
iex(4)> hour
20

Matching constants

這很無聊,就是自己比對自己,得到的答案會是自己

1
2
iex(1)> 1 = 1
1

但我們今天把 person 匹配給 {:person, "Bob", 25},並且自己比對自己,我們可以發現,比對成功!

1
2
3
4
iex(2)> person = {:person, "Bob", 25}

iex(3)> {:person, name, age} = person
{:person, "Bob", 25}

Matching lists

1
2
3
4
5
6
7
8
9
10
iex(1)> [first, second, third] = [1, 2, 3]
[1, 2, 3]

iex(3)> [head | tail] = [1, 2, 3]
[1, 2, 3]

iex(4)> head
1
iex(5)> tail
[2, 3]

Matching maps

1
2
3
4
5
6
7
8
iex(1)> %{name: name, age: age} = %{name: "Bob", age: 25}
%{age: 25, name: "Bob"}

iex(2)> name
"Bob"

iex(3)> age
25

要注意一點是,在 map 中可以不用全部匹配,以下這樣部分匹配也行,但沒有 key 是匹配不起來的

1
2
3
4
iex(4)> %{age: age} = %{name: "Bob", age: 25}

iex(5)> age
25
1
2
iex(6)> %{age: age, works_at: works_at} = %{name: "Bob", age: 25}
** (MatchError) no match of right hand side value

Matching with functions

我們來看一個函式長怎麼樣子

1
2
3
def my_fun(arg1, arg2) do
...
end

在這裡的 arg1 和 arg2 是 patterns,而 term 就是我們在呼叫它的時候輸入的變數

1
2
3
4
5
defmodule TestModule do
def my_fun(val1, val2) do
val1 * val2
end
end

我們可以這樣

1
2
iex(1)> TestModule.my_fun(2, 3)
6

但我們不能這樣,這樣會造成匹配失敗

1
iex(1)> TestModule.my_fun({2, 3})

接下來我們來介紹 Matching with functions 的一個延伸應用,在一般的程式語言中,如果我們要使用條件分支,我們或許可以這樣寫

1
2
3
4
5
function test(x){
if (x < 0) return "negative";
if (x == 0) return "zero";
return "positive";
}

但到了 Elixir 中,我們必須改成變一下寫法,我們可以使用 Multiclause functions 來達到一樣的效果,如下

1
2
3
4
5
defmodule TestNum do
def test(x) when x < 0, do: :negative
def test(0), do: :zero
def test(x), do: :positive
end

所謂的 Multiclause functions 其實就是多個 Matching with function,藉由 pattern matching 讓每一個條件都可以進入一個 function 中處理事情,達到一樣條件分支的效果喔

Multiclause functions

在 Elixir 中允許 overload,也就是同一名稱函式可以出現多次,差別只在輸入的變數不一樣,假設今天我們要計算面積,不管是圓型或是長方形都要使用同一個函式來計算,那我們可以這樣做

1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule Geometry do
def area({:rectangle, a, b}) do
a * b
end

def area({:square, a}) do
a * a
end

def area({:circle, r}) do
r * r * 3.14
end
end

我們可以看到,在 Geometry 這個 Module 中定義了 rectangle、square 和 circle,唯獨沒有定義 triangle,所以當我們在 iex 上嘗試想要輸入 {:triangle, 1, 2, 3} 的時候會失敗,因為我們匹配不到任何東西

1
2
3
4
5
6
7
8
9
iex(1)> Geometry.area({:rectangle, 4, 5})
20
iex(2)> Geometry.area({:square, 5})
25
iex(3)> Geometry.area({:circle, 4})
50.24
iex(4)> Geometry.area({:triangle, 1, 2, 3})
** (FunctionClauseError) no function clause matching in Geometry.area/1
geometry.ex:2: Geometry.area({:triangle, 1, 2, 3})

那好假設我們不想要出現 error 要什麼修改 Module 呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defmodule Geometry do
def area({:rectangle, a, b}) do
a * b
end

def area({:square, a}) do
a * a
end

def area({:circle, r}) do
r * r * 3.14
end

def area(unknown) do
{:error, {:unknown_shape, unknown}}
end
end

我們新增一個 pattern,讓任何奇怪形狀的輸入都可進到 area(unknown) 來,這樣一來所有的 input 皆可被捕捉到啦

注意,area(unknown) 不可以放在 Module 的第一行當作第一個函式,這樣所有的 input 都只會跑到第一個 pattern 內,而不會進入第二第三個 pattern

1
2
3
4
5
iex(1)> Geometry.area({:square, 5})
25

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

Multiclause lambdas

一個正常的匿名函式(lambda)長這樣

1
2
3
iex(1)> double = fn x -> x*2 end
iex(2)> double.(3)
6

讓我們讓它變得更豐富一點,變成 Multiclause lambdas

1
2
3
4
5
6
7
8
iex(3)> test_num =
fn
x when is_number(x) and x < 0 ->
:negative
0 -> :zero
x when is_number(x) and x > 0 ->
:positive
end

那我們可以測試一下

1
2
3
4
5
6
iex(4)> test_num.(-1)
:negative
iex(5)> test_num.(0)
:zero
iex(6)> test_num.(1)
:positive

參考資料

  1. https://elixirschool.com/en/lessons/basics/pattern-matching/