Currying 是 FP 的招牌菜,僅管了解 Currying 的定義,但似乎還是很難將 Currying 用在實務上;本文藉由 Ramda 的 Function 設計,從更務實角度學習 Ramda 怎麼發揮 Currying。
Version
macOS Mojave 10.15.2
VS Code 1.41.1
Quokka 1.0.274
Ramda 0.26.1
Ramda
Ramda 是一個將 currying 用到極致的 library,所有 function 都是天生 currying,觀察 Ramda 的設計,我們可以發現每個 function 都具有兩到三項功能:
- 若將 argument 都填滿求出 result
- 若故意缺最後一個 argument,則回傳另一個以所缺 argument 構成的新 function,這是 Ramda 能 point-free 的理論基礎
- 若故意缺最後兩個 argument,亦回傳另一個新 function
Ramda function 絕大部分都是兩個 argument,因此 1
與 2
都成立,少數 function 有三個以上 argument,則 3
才會成立
由於 currying,使得每個 function 都最少有兩個功能以上,並不像 OOP 的 method 只有單一功能
接下來實際舉幾個 Ramda 使用 currying 的例子。
prop()
prop()
為實務上常用 function,負責從 object 讀取特定 property。
let obj = { title: 'FP in JavaScript', price: 100 }
obj.title // ?
傳統 OOP 是以 .
的方式讀取 object 的 property。
OOP 與 imperative 習慣以大量 operator 操作
import { prop } from 'ramda'
let obj = { title: 'FP in JavaScript', price: 100 }
prop('title')(obj) // ?
Ramda 則提供了 prop()
來讀取 object 的 property。
prop()
s -> {s: a} -> a | undefined
傳入 property 與 object,傳回 value
表面上看到的功能,是傳入 property 與 object,會回傳 value,也就是 1
的功能。
乍看之下覺得沒什麼了不起,只是從
.
變成prop()
而已,但 Ramda 還有後著
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let fn = (pr, arr) => arr.map(x => x[pr])
fn('title', data) // ?
若使用 ECMAScript,我們會使用 Array.prototype.map()
搭配 []
動態存取 property。
import { map } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let fn = pr => map(x => x[pr])
fn('title')(data) // ?
若使用 Ramda 的 map()
,則 data 可 point-free,map function 仍可使用 arrow function 與 []
動態存取 property。
import { map, prop } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let fn = pr => map(prop(pr))
fn('title')(data) // ?
但實務上常看到的 prop()
卻只提供第一個 argument,然後回傳 {s: a} -> a
,剛好提供了 map function 所要的 signature,這也是 Ramda 與其他 library 不同之處:使得 point-free 成為可能,也就是 2
的功能。
這一切都歸功於 currying,使得
prop()
有了兩種功能:可以求值,亦可以產生 callback。
import { map, prop, compose } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let fn = compose(map, prop)
fn('title')(data) // ?
這種 f(g(x))
形式常常看到,這正是 function composition 基本型,因此可使用 compose()
將 prop()
與 map()
組合成新 function。
propEq()
propEq()
也是實務上常用的 function,負責判斷 object 的 property 是否等於特定值。
let obj = { title: 'FP in JavaScript', price: 100 }
obj.title === 'FP in JavaScript' // ?
傳統 OOP 是以 .
的方式讀取 object 的 property,並使用 ===
判斷。
import { propEq } from 'ramda'
let obj = { title: 'FP in JavaScript', price: 100 }
propEq('title')('FP in JavaScript')(obj) // ?
Ramda 提供了 propEq()
來判斷 property 是否相等於特定值。
propEq()
String -> a -> Object -> Boolean
比對 object 的 property 與 value 是否相等
表面上看到的功能是傳入 property 、value 與 object,會回傳 Boolean,也就是 1
的功能。
乍看之下覺得沒什麼了不起,只是從
.
與===
變成propEq()
而已,但 Ramda 還有後著
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
];
let fn = (val, arr) => arr.filter(x => x.title === val)
fn('RxJS in Action', data) // ?
若使用 ECMAScript,我們會使用 Array.prototype.filter()
搭配 arrow function。
import { filter } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let fn = val => filter(x => x.title === val)
fn('RxJS in Action')(data) // ?
若使用 Ramda 的 filter()
,則 data 可 point-free,predicate 仍可使用 arrow function。
import { filter, propEq } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let fn = val => filter(propEq('title')(val))
fn('RxJS in Action')(data) // ?
若將 propEq()
只填兩個 argument,可將 predicate 成為 point-free。
import { filter, propEq, compose } from 'ramda'
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let fn = compose(filter, propEq('title'))
fn('RxJS in Action')(data) // ?
propEq()
是 Ramda 少數有三個 argument 的 function,因此我們有了另一種玩法:只提供一個 argument。
若 propEq()
只提供了一個 argument,因此產生了 a -> Object -> Boolean
的新 function,因此我們可以用 compose()
將 propEq('title')
與 filter()
組合成新 function。
這樣連 fn()
也 point-free 了。
這一切都歸功於 currying,使得
propEq()
有了三種功能:可以求值,可以產生 callback,亦可產生新 function
Conclusion
- 若沒有 currying,則 method 也好,function 也好,都只能提供單一功能
- 若有 currying,且將 data 放在最後一個 argument,故意缺最後一個 argument 可以產生 callback,故意缺最後兩個 argument 可以產生新 function
- Currying 使得 function 提供多種功能