等號究竟是什麼意思? Elixir 裡的 pattern matching
image via Imaiges
上次的敝司的~~大頭目~~紅寶石鑑定商寫了 你看過 Elixir 嗎?如果沒有,現在讓你看看!,介紹了 Elixir 這個語言許多有趣的特色。我想要接著來談這門語言另一個非常核心的元素:Pattern matching。
由於 Ruby (及新的 JavaScript ES6) 都有 destructive assignment,所以可以這樣指派變數:
# Ruby
a, b = [1, 2]
#=> a 是 1, b 是 2
那如果我們這麼做, a
會是多少呢?
a, a, b = [1, 2, 3]
答案是 a
是 2
。因為 a
先被指派為 1
,緊接著又被指派為 2
。
上次文章中有提到過函數式編程 (functional programming) 裡的 functional 這個字,指的是數學上的函數。而在數學上,當你說 y = ax + b
時,那個等號 =
,並不是我們在 Ruby, JavaScript 語言裡學到的指派。而是說當輸入 x 時,y 會依函式的定義,計算出一個固定值。一樣的 x 輸入,永遠會是一樣的結果。
如果我們在 Elixir 上試著寫出一樣的程式,會發生什麼事呢? “`elixir [a, a, b] = [1, 2, 3]
#=> ** (MatchError) no match of right hand side value: [1,2,3] ”`
Elixir 告訴你,「同學同學,a
不可能同時是 1
又是 2
。~~你的數學老師在你後面,他非常火。~~」
Pattern Matching: 找個辦法讓等號左右兩邊比對成功
但是如果我們把上面的例子改寫成這樣,Elixir 就會接受了: “`elixir [a, a, b] = [1, 1, 3]
#=> [1, 1, 3] ”`
在 Elixir 中的等號,並不是單純的變數指派。而是 pattern matching,樣式比對:
把等號左邊的模式,跟右邊的值比對看看。如果有辦法找到讓等式成立的情況,那就幫你綁定變數。
而讓我們剛剛那個等式成真的方法,就是讓 a
綁定成 1
,b
綁定成 3
。
所以也可以這樣用:
[a, 3, b] = [1, 2, 3]
\#=> ** (MatchError) no match of right hand side value: [1,2,3]
[a, 2, b] = [1, 2, 3]
\#=> [1,2,3]
串列比對與連續比對
[head | tails] = [1, 2, 3, 4]
\#=> head 是 1,tails 是 [2, 3, 4]
|
是串列比對中切開首值及剩下的串列的方式,如同 LISP 中的 car
及 cdr
。
而由於每個比對本身會是一個有回傳值的 expression,那麼可以一直比下去也是很合理的: “`elixir [h | t] = [1, b, c, d] = [1, 2, 3, 4]
#=> b = 2, c = 3, d = 4 #=> h = 1, t = [2, 3, 4] ”`
只要最右邊能求出所有的值,中間的比對都成功,那麼就可以一次綁定多個變數了。
_
: 我不在乎這個
在 pattern matching 時,常常會遇到你只想要確保一部份的值或是資料結構,而不在乎其它地方,就可以利用 _
來進行 pattern matching。 例如我只想要將串列中前兩個元素綁定成變數,後面有什麼干我 P 事,就可以這樣做:
[a, b | _] = [1, 2, 3, 4, 5]
\#=> a = 1, b = 2
一個比對裡,可以使用多個 _
。如果覺得這樣讓程式很難讀,可以改成用底線開頭的變數,效果是一樣的。
[first, _second, third | _tails] = [1, 2, 3, 4, 5]
#=> first = 1, third = 3
#=> 雖然可以用 _second 及 _tails 拿到值,但是 Elixir 會噴警告給你。
^
: 將變數求值進行比對
當某個變數已經被綁定好值了,你接著想要用這個值來進行比對,可以使用 Pin operator: ^
a = 0
[^a, b, 2 | _tails] = [0, 1, 2, 3, 4]
\#=> b = 1
函式的 pattern matching
除了等號之外,Elixir 函式定義的參數部份,也是個 pattern matching。
defmodule Shape do
def area({:square, w}), do: w * w
def area({:circle, r}), do: r * r * 3.14
def area({:rectangle, w, h}), do: w * h
end
Shape.area({:square, 5}) #=> 25
Shape.area({:circle, 10}) #=> 314
Shape.area({:rectangle, 5, 10}) #=> 50
在呼叫函式時,會由上而下用參數去比對函式的每一個區塊 (clause),如果找到可以成功比對的,就進入該區塊執行。
還可以多加一點判斷
除了在函式宣告的參數部份比對之外,Elixir 還提供了Guards 來做稍微複雜一點的判斷:
defmodule Alcohol do
def check_age(_, age) when age < 18, do: "你年紀太小囉"
def check_age(name, _), do: "歡迎,#{name}"
end
所以要寫出遞迴版本的費波那契數列,在 Elixir 裡就很簡單了:
defmodule Fibonacci do
def get(1) when n == 0 or n == 1, do: n
def get(n), do: get(n - 1) + get(n - 2)
def list(n), do: for x <- 0..n - 1, do: get(x)
end
小結
Pattern matching 在 Elixir (及許多函數式編程語言,如 Haskell) 中扮演了非常重要的角色。可以讓你寫出非常簡潔而且易懂的程式,而且會讓你開始習慣用找出邊界情況及遞迴的角度,來思考如何組織程式。
工商服務
當然最後還是要廣告一下,今年四月 Ruby X Elixir Conf Taiwan 2018 研討會,有邀請到 Elixir 的作者 José Valim,還有許多精采的相關講題喔!我們近期也會舉辦相關的 Elixir 活動,就敬請持續關注我們的活動通知吧!
Happy hacking! 下次見。