iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 8
1
Software Development

函數式編程: 從 Elixir & Phoenix 入門。系列 第 8

函式真正的名字、捕獲運算子及 partial application

具名函式的引用名稱

在 Elixir 中,要稱呼一個具名函式,有個固定的格式:模組.函式名/參數個數。這個名稱格式廣泛用於文件及網路上的溝通。打開 iex 試試看:

iex(0)> h Enum.map/2
# 會印出該函式的文件...

若你定義了接收不同參數個數的同名函式,或是宣告了有預設式的函式,都會產生兩種不同參數個數的檔案名稱,例如內建的集合處理函式,就有 Enum.reduce/3Enum.reduce/2 兩種,分別是需要傳入起始值,及只接收兩個參數,直接用集合第一個元素當起始值的。

& 捕獲運算子 (capture operator)

再來看看 JavaScript ,具名函式可以直接傳遞。所以遇到在 lambda 中將接收到的參數原封不動傳給具名函式,並回傳其結果的情況下,可以直接傳遞具名函式。這就是之前提到的 λ-calculus 裡的一條規則,叫做 eta reduction (η-reduction)。講起來很拗口,看範例就很直覺:

/* JavaScript */

[-1, -2, 3].map(i => Math.abs(i))
// η-reduction
[-1, -2, 3].map(Math.abs)

但在 Elixir 中,具名函式不加括號視同零參數的呼叫,因此我們需要有辦法將具名函式轉換成 lambda。這就是 & 的第一個用法。在轉換其它 module 的函式時,語法就是 & 加上剛剛提到的函式正式名稱: &Module.function/arity

Enum.map([:a, :b, :c], fn a -> Atom.to_string(a) end)
# η-reduction
Enum.map([:a, :b, :c], &Atom.to_string/1)

當然轉換模組內的 function 或是用 import 語法匯入的函式也沒問題,不加 Module 名稱就可以。

def double(i), do: i * 2
Enum.map([1, 2, 3], &double/1)
#=> [2, 4, 6]

順帶一提,用 & 捕獲/生成的函式不一定要寫在高階函式中,也可以另外指派給變數。由於被轉換成匿名函式了,所以呼叫時要用 .()

f = &Kernel.is_atom/1
f.(:atom) #=> true

更短的匿名函式

昨天提到了匿名函式的宣告是 fn -> end ,其實相當冗長。因此遇到函式本體很短的情況下會覺得麻煩。

Enum.map([1, 2, 3], fn i -> i * 2 end)

這種情況就是 & 運算子派上用場的另一個地方,順帶一提,也是最多人感到困惑的用法。&1 是函式接收到的參數。

Enum.map([1, 2, 3], &(&1 * 2))
#=> [2, 4, 6]

換句話說,fn i -> i * 2 end&(&1 * 2) 是一樣的意思。

更多參數

既然有 &1, 可以推測出要多個參數也是可以的:

fn = &(&1 + &2 + &3)

fn.(1, 2, 3) #=> 6

生成 List 或 Tuple

若用 [] 或 {} 代替圓括號,呼叫後的結果會分別是 List 及 Tuple

l = &[&1, &2]
l.(1, 2)
#=> [1, 2]

t = &{&1, &2}
t.(1, 2)
#=> {1, 2}

&() 的使用判準

雖然短的很方便,濫用 &()語法的話,程式很容易就會變得難讀。我個人的判準是內部超過 10 個字元,或是有三個以上的運算子,就寧願用 fn -> end 來宣告了。

那麼要做出 Haskell 的 identity 就簡單了: &(&1)

Enum.group_by(["a", "b", "c", "a", "b"], &(&1))

#=> %{"a" => ["a", "a"], "b" => ["b", "b"], "c" => ["c"]}

如果你傳了不夠的參數…

想一下,如果有個函式需要三個參數,你只傳一個參數給它,會發生什麼事?寫 JavaScript 的人會說「其它的參數會是 undefined」。其它 OO 語言的人會說「會噴錯誤」。但真的只能這樣嗎?

在 functional programming 裡,有一個概念叫 partial application。就是當你呼叫函式,但只傳進部份的參數時,他會綁定你傳入的參數,但回傳另一個需要剩下的參數的函式。因為 arguments are partially applied,這個行為的名詞化就叫 partial application。雖然有人翻成部份應用,但我覺得這個詞翻譯了反而更難懂。在 elixir 裡,就是透過 & 來做這件事:

take_five = &Enum.take(&1, 5)
take_five.(1..100)

# => [1, 2, 3, 4, 5]

partial application 常常一起提到的另一個詞叫 curry,柯里化。跟那種棕色的好吃食物沒有關係,這個字是說把接收 n 個參數的函式,轉換成 n 層只接收一個參數的函式

在 Elixir 裡,沒有內建 curry 函式,而且因為 & 不支援嵌套,而且呼叫匿名函式要用 .(),所以寫起來不太漂亮:

add_all = fn a -> fn b -> fn c ->  a + b + c end end end
add_all.(1).(2).(3)
#=> 6

之前提到的 JavaScript FP 函式庫 Ramda 就有提供 curry 函式幫你把一般函式柯里化。而在 ES6 裡,也可以用箭頭函式來宣告:

/* JavaScript */

add_all = a => b => c => a + b + c

add_all(1)(2)(3)
#=> 6

重點回顧

  • Module.function/arity 來指稱一個函式
  • 在 iex 裡用 h/1 可以印出函式說明
  • & 運算子可以把具名函式轉成 lambda 來傳遞
  • & 也可以用來寫出很短的函式
  • & 還可以拿來做 partial application
  • Elixir 對 curry 不太友善 (咦

函式宣告的部份就到此為止了。明天將開始介紹各種資料型別了。

Happy hacking! 明天見。


上一篇
匿名函式
下一篇
基本型別及運算
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

尚未有邦友留言

立即登入留言