iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 14
1

幸福的家庭都是相似的,而不幸的家庭各有各的不幸。

--列夫·托爾斯泰, 安娜·卡列尼娜


那麼,等號 = 除了綁定還能做什麼呢?我們可以先回顧一下 JS 莊園裡一個叫 解構賦值 (destructive assignment) 的功能。他可以在指派變數的時候,直接把右邊的資料結構拆開,並拿出其中的部份,並分別指派成想取的變數名稱。

// JavaScript 語法

let [a, b, c] = [1, 2, 3]
//=> a 被指派到 1, b 被指派到 2, c 被指派到 3。

let {x: x, y: y, z: z} = {x: 1, y: 2, z: 3}
//=> x 被指派到 1, y 被指派到 2, z 被指派到 3。

let {m: m, n: n} = {m: 1, n: 2, o: 3}
// 只拆一部份也可以
//=> m 被指派到 1, n 被指派到 2。

胡亂搞一下

那在 JS 莊園裡,如果寫出下面的東西,ab 分別會被指派成什麼呢?

// JavaScript 語法
var [a, a, b] = [1, 2, 3]

「b 是 3,a 是… 2 吧?因為 a 先被指派成 1 接著又被改成 2。」

等號是…左右比對

但是在數學上,[a, a, b] = [1, 2, 3] 看起來根本就不合理。

記得我們之前說了,Elixir 在變數的設計上,採用了比較接近是數學的 immutable 做法嗎?而談到 Elixir 的等號 = ,除了變數綁定之外,其實還有一個非常關鍵的概念,叫做模式比對 (pattern matching)

當你試著運行 [a, a, b] = [1, 2, 3] 時, Elixir 會直接噴錯誤給你看。說這在數學上是矛盾的,等號的左右兩邊並沒有辦法對上 (MatchError)

# Elixir 語法
[a, a, b] = [1, 2, 3] 
# 錯誤!
#=> ** (MatchError) no match of right hand side value: [1, 2, 3]

但是如果我們改成這樣,那麼它就可以接受了。讓這個等式成真的方法,就是把 a綁定成 1,而 b 綁定成 3

# Elixir 語法
[a, a, b] = [1, 1, 3] 
#=> a 被綁定成 1, b 被綁定成 3

而既然是比對,你也可以這樣做, 這樣一來,只有等號右邊的值的第二個元素正好是 2 的時候,才能比對成功。

# Elixir 語法
[a, 2, b] = [1, 2, 3] 
#=> a 被綁定成 1, b 被綁定成 3

Elixir (以及許多函數式國度) 的等號都有這種特色,它同時幫你檢查等式是否有辦法成真,如果可以的話,就幫你綁定變數。如果無法成真時,就告訴你這是錯的。


拿出頭跟尾巴

不知道你有沒有注意到,在 Elixir 中 [] 代表的是串列,所以沒辦法用 index 直接拿到我們想要的第 n 個元素,而是得要用之前說過的取出頭及尾巴的方式,拿到裡面的元素。而要做到這件事,也是用 pattern matching 來進行的。

# Elixir 語法
[head | tail] = [1, 2, 3, 4, 5] 
#=> head 被綁定為 1,tail 被綁定為 [2, 3, 4, 5]

[fst, snd, trd | rest] = [1, 2, 3, 4, 5, 6]
#=> fst 被綁定為 1,snd 被綁定為 2,trd 被綁定為 3, rest 則是 [4, 5]

元組與比對

除了串列之外,函數式的國度常常有一種資料結構,叫做元組 Tuple。這個跟串列很像,但是長度固定。在 Elixir 中,最常拿來做為錯誤處理使用。對於有可能會出錯的函式呼叫,多半會回傳 {:ok, 成功的結果} 或是 {:error, 錯誤訊息}。那麼你只要對它做 pattern matching,就可以確定拿到的變數是成功的結果。

# Elixir 語法
{name, age} = {"Pon-pon", 1} # => name = "Pon-pon", age = 1

{:ok, contents} = File.read("my_app.config") # MatchError if {:error, _something}

Map 與其比對

由於在 Elixir 中 {} 代表的是 Tuple,而像是 JS 莊園鍵值對的東西,在這裡叫做 Map,是用 %{} 來表示的,而它當然可以用 pattern matching 取出裡面的值。

# Elixir 語法
%{x: x, y: y} = %{x: 1, y: 2, z: 3} 
#=> x 被綁定為 1, y 被綁定為 2

由於等號的左邊可以放值或變數,所以利用這個特性,你可以在某些值是特定的值才進行綁定,不然就直接 MatchError:

# Elixir 語法
%{name: name, age: 18} = %{name: "Ash", age: 18} # 成功綁定

%{name: name, age: 18} = %{name: "Bob", age: 20} # MatchError

連續的比對

每一次的比對,其實也是一段會回傳值的敘述句。因此你可以再用另一個等號去接住其結果。當然這也意味著你可以用連續的 pattern matching,一次就將想要用的變數都綁定好。

# Elixir 語法
html =
  %{
    head: head = %{title: title},
    body: body
  } = %{
    head: %{title: "foo", meta: "bar"},
    body: "Hello world"
  }

# body 被綁定為 "hello world"
# title 被綁定為 "foo"
# head 被綁定為 %{title: "foo", meta: "bar"}
# html 被綁定為 %{head: %{title: "foo", meta: "bar"}, body: "Hello world"}

到處都是 pattern matching

而除了等號之外,在 Elixir 中,pattern matching 基本上可說是無所不在。例如 casetry...catch。還有 with 語句。用在想讓多個前題都成真,才繼續往下處理,而任何一步錯誤,都會流向同一個出口的 rails oriented programming 設計上:

# Elixir 語法
with {:ok, number} <- Map.fetch(m, :a),
     {:ok, result} <- some_thing_danger(number) do
    continue_process(result)
else
  error ->
    IO.puts("Something went wrong: #{error}")
    :error
end

https://ithelp.ithome.com.tw/upload/images/20200929/201033901RGVsIydn4.png


當然說到 pattern matching,除了這些之外,還有一個最重要的,不得不提的用處:

函式。在函式的定義上,也可以用到模式比對…


我注意到另一個告示板,上面寫的並不是單純的數字,而是相當複雜的資料的結構。而那些移動的盒子們,伸出等號與告示板取得資訊後,裝在盒子裡的,有時是整個結構,有時是單純的數字,也有時是資料的一小部份而己。

而非常偶爾,也會看到在等號接觸後,盒子變成透明的紅色,然後分解…消失…


上一篇
mostly:functional 第十二章:鐫刻的真相,狀態的琥珀
下一篇
mostly:functional 第十四章:再一次遞迴,然後…
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
taiansu
iT邦新手 3 級 ‧ 2020-09-28 21:35:40

註*:給看到這個標題覺得應該要開始講 homotopy type theory, HoTT 的人:你是對的,我也這麼覺得。但我對那個除了知道它的存在之外,可以說是一無所知,抱歉讓你失望了。如果你拿這個標題寫一篇我一定去幫你按讚分享。

我要留言

立即登入留言