iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 3
3

function? 不是大家每天都在寫嗎?

曾經有人問說「只要會寫程式的人,都一定會定義 function 的啊!那我都用 function 寫程式,就是 functional programming 了嗎?」

不,並不是這樣的。

在物件導向的程式語言裡,認為資料結構及操作這種資料結構的行為,應該要放在同一個地方, 這就是物件的初衷。有了物件的定義 (class or prototype) 後,該物件擁有用來操作自身資料 (或稱狀態) 的行為,也就是函式,通常會被稱作方法 (method)。

方法一定是函式,但反過來就不一定成立了。例如在 JavaScript 這個有趣的語言中,函式是一級成員。不需要隸屬於一個物件,不一定要有名稱(匿名函式),可以當成其它函式的參數來傳遞,也可以做為回傳值。在其它的語言裡,常常用 lambda 這種名稱/方式來處理這種一級公民函式的概念。

λ:那個拖著腳的人

lambda 這個字,來自數學上的 λ-calculus (lambda calculus)。在三零年代由 Alonzo Church 發表,是一套從數學邏輯中發展出來的變數綁定及替換規則。由此規則推出一個函數抽象化定義、函數應用及遞迴的形式系統。

所以 functional programming 裡的第一個字,並非程式語言裡的概念,而是數學上的函數 (mathematical function),也就是國中課本裡的:

https://ithelp.ithome.com.tw/upload/images/20171222/20103390dC25Xdkk6l.png

這個型式系統透過後來的柯里-霍華德同構推導出寫(某些)程式跟證明數學是同一件事。這麼一來,已發展的相關數學理論與解法,都可以實現在程式上。反之也可以用寫程式的方式處理數學證明。如果有一天你開始學 Haskell,會發現大家都在講 monad, functor 這些範疇論的概念,那就要恭喜你踏入函數式編程的更高境界了。

等號 = 究竟是什麼意思

可能很多人聽到數學就退避三舍,這時候就要拿 Haskell 圈裡的慣例出來用:

其實這很簡單,不要想得太複雜。
-- 放大絕前的招式咏唱

開玩笑的,現在這個真的很簡單。在數學公式裡,當我們說 y = ax + b 時,那個等號並不是在學 C, Ruby , Python, JavaScript 時說的變數指派,而是說對一個收斂函數來說,當輸入的 x 變化時,函式的輸出 y總是相同的值。而這就是我們今天要探討的主題,pattern matching,模式比對。

=: 不是指派,而是模式比對

在 Elixir 中,你可以寫 a = 1,看起來及運作起來都跟變數指派一模一樣,但其實它有完全不同的含義:

把等號左邊的模式,跟右邊的值比對看看。如果有辦法找到讓等式成立的情況,那就幫你綁定變數。

在 JavaScript ES6 中引進了 destruct assignment,解構賦值。有些人以為這就是 patter matching,但其實還差了一點,看看以下的範例:

var [a, a, b] = [1, 2, 3]
// a => 2
// b => 3

因為等號只是單純的指派, a 先被指派成 1,緊接著又被指派成 2

而在 Erlang/Elixir 中,一樣的輸入,會發生這樣的結果:

[a, a, b] = [1, 2, 3]
#=> ** (MatchError) no match of right hand side value: [1,2,3]

Elixir 用這訊息告訴你,它找不到方法讓這個等式成真, a 不可能同時是 1 又是 2。這次比對是失敗的。

若將右邊的值改成這樣:

[a, a, b] = [1, 1, 3]
# => [1, 1, 3]

讓這個等式成真的方法,就是讓 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]

現在你應該可以猜到,這是串列比對中的切開首值及剩下的串列的方式,如同 LISP 中的 carcdr。更多關於串列的細節,會在後面的章節討論。

每個比對本身會是一個有回傳值的 expression,那麼可以一直比下去也是很合理的:

[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

本日重點回顧

  • 函數式編程的 functional 是指數學函數的
  • 數學裡的 λ-calculus、範疇論 (category theory) 跟寫程式是緊密相關的
  • 等號 = 是 pattern matching,若存在讓等式成真的解,就進行變數綁定
  • 比對失敗時,會產生 MatchError
  • 單變數比對任何值都會成功
  • | 來比對串列的尾巴
  • 對於不在乎的部份可以用 _ 去敷衍它 (咦
  • 想用已綁定的變數進行比對,用 pin operator: ^

Pattern matching 是 Elixir/Erlang 語法裡最核心的部份。明天將會就這個主題繼續延伸,討論函式上的模式比對,以及遞迴。

Happy hacking! 明天見啦!


上一篇
環境安裝
下一篇
函式、模組,還有那些會跳針的。
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

尚未有邦友留言

立即登入留言