點燈坊

失くすものさえない今が強くなるチャンスよ

學習 Ramda 使用 Currying 設計 API

Sam Xiao's Avatar 2020-01-24

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 都具有兩到三項功能:

  1. 若將 argument 都填滿求出 result
  2. 若故意缺最後一個 argument,則回傳另一個以所缺 argument 構成的新 function,這是 Ramda 能 point-free 的理論基礎
  3. 若故意缺最後兩個 argument,亦回傳另一個新 function

Ramda function 絕大部分都是兩個 argument,因此 12 都成立,少數 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 操作

currying000

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 還有後著

currying001

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。

currying009

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。

currying002

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。

currying003

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。

currying010

propEq()

propEq() 也是實務上常用的 function,負責判斷 object 的 property 是否等於特定值。

let obj = { title: 'FP in JavaScript', price: 100 }

obj.title === 'FP in JavaScript' // ?

傳統 OOP 是以 . 的方式讀取 object 的 property,並使用 === 判斷。

currying006

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 還有後著

currying007

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。

currying011

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。

currying008

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。

currying004

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

currying005

Conclusion

  • 若沒有 currying,則 method 也好,function 也好,都只能提供單一功能
  • 若有 currying,且將 data 放在最後一個 argument,故意缺最後一個 argument 可以產生 callback,故意缺最後兩個 argument 可以產生新 function
  • Currying 使得 function 提供多種功能

Reference

Ramda, prop()
Ramda, propEq()