等號究竟是什麼意思? 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]

答案是 a2。因為 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 綁定成 1b 綁定成 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 中的 carcdr

而由於每個比對本身會是一個有回傳值的 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) 中扮演了非常重要的角色。可以讓你寫出非常簡潔而且易懂的程式,而且會讓你開始習慣用找出邊界情況遞迴的角度,來思考如何組織程式。


工商服務

RubyElixirConf Taiwan

當然最後還是要廣告一下,今年四月 Ruby X Elixir Conf Taiwan 2018 研討會,有邀請到 Elixir 的作者 José Valim,還有許多精采的相關講題喔!我們近期也會舉辦相關的 Elixir 活動,就敬請持續關注我們的活動通知吧!

Happy hacking! 下次見。