一直很羨慕 F# 的 List
Module 提供了豐富的 Function,而 ECMAScript 的 Array.prototype
卻只提供有限的 Function 可用,因此無法完全發揮 FP 威力。但這一切終於得到解決,Ramda 擁有豐富的 Function,且很容易自行開發 Function 與 Ramda 整合實現 Function Pipeline。
Version
Ramda 0.27.1
Imperative
let data = [
{ title: 'fp in JavaScript', price: 100 },
{ title: 'rxJS in Action', price: 200 },
{ title: 'speaking JavaScript', price: 300 }
]
let f = (x, a) => {
let result = []
for (let i = 0; i < a.length; i++) {
if (a[i].price === x) {
result.push(a[i].title)
}
}
return result
}
f(300, data) // ?
很簡單的需求,data
有各書籍資料,包含 title
與 price
,我們想得到 price
為 300
的資料,且只要 title
即可。
若使用 Imperative 寫法,我們會使用 for
loop,先建立要回傳的 result
Array,由 if
去判斷 price === 300
,再將符合條件的 title
寫入 result
Array,最後再回傳。
Array.Prototype
let data = [
{ title: 'fp in JavaScript', price: 100 },
{ title: 'rxJS in Action', price: 200 },
{ title: 'speaking JavaScript', price: 300 }
];
let f = (v, a) =>
a
.filter(x => x.price === v)
.map(x => x.title);
f(300, data) // ?
熟悉 FP 的讀者會很敏感發現,這就是典型 filter()
與 map()
而已,我們可直接使用 ECMAScript 在 Array.prototype
內建的 filter()
與 map()
即可完成。
Ramda
import { pipe, filter, map } from 'ramda'
let data = [
{ title: 'fp in JavaScript', price: 100 },
{ title: 'rxJS in Action', price: 200 },
{ title: 'speaking JavaScript', price: 300 }
]
let f = v => pipe(
filter(x => x.price === v),
map(x => x.title)
)
f(300)(data); // ?
Ramda 身為 functional library,內建 filter()
與 map()
自然不在話下。
我們可想像 data 先經過 filter()
處理,再將結果傳到 map()
,如此 Function Pipeline 過程可使用 pipe()
加以整合。
至於 filter()
與 map()
要傳入的 callback,可使用 arrow function。
我們發現
f()
的a
argument 不見了,稱為 Point-free,讓程式碼更精簡
Point-free
import { pipe, filter, map, propEq, prop } from 'ramda'
let data = [
{ title: 'fp in JavaScript', price: 100 },
{ title: 'rxJS in Action', price: 200 },
{ title: 'speaking JavaScript', price: 300 }
]
let f = x => pipe(
filter(propEq('price', x)),
map(prop('title'))
)
f(300)(data) // ?
既然 f()
能 Point-free,filter()
與 map()
的 callback 也能 point-free 嗎 ?
- 由
proEq('price', v)
產生x => x.price === v
- 由
prop('title')
產生x => x.title
如此 callback 也 Point-free 了,不只更精簡,且可讀性更高。
User Function
import { pipe, filter, map, propEq, prop } from 'ramda'
let data = [
{ title: 'fp in JavaScript', price: 100 },
{ title: 'rxJS in Action', price: 200 },
{ title: 'speaking JavaScript', price: 300 }
]
let capitalize = x => x[0].toUpperCase() + x.slice(1);
let f = x => pipe(
filter(propEq('price', x)),
map(prop('title')),
map(capitalize)
)
f(300)(data) // ?
眼尖的讀者會發現結果的 speaking JavaScript
的 s
為小寫,原始的資料就是如此,但 s
為大寫較符合英文閱讀習慣。
因此我們可以自行寫一個 capitalize()
將第一個字母變成大寫。
在 Ramda 要成為能夠 pipe()
的條件很簡單,只要 function 是單一 argument,且是 pure function 即可。
Point-free
import { pipe, filter, map, propEq, prop, adjust, join, toUpper } from 'ramda'
let data = [
{ title: 'fp in JavaScript', price: 100 },
{ title: 'rxJS in Action', price: 200 },
{ title: 'speaking JavaScript', price: 300 }
]
let capitalize = pipe(
adjust(0, toUpper),
join('')
)
let f = x => pipe(
filter(propEq('price', x)),
map(prop('title')),
map(capitalize)
)
f(300)(data) // ?
第 9 行
let capitalize = pipe(
adjust(0, toUpper),
join('')
)
capitalize()
亦可以 Point-free 實現:
adjust(0, toUpper)
:將 String 視為 Char Array,將char[0]
以toUpper()
轉大小寫,回傳為 Arrayjoin('')
:將 Array 轉成 String
Conclusion
- Ramda 提供了 FP 該有的 function,不再侷限於
Array.prototype
- Ramda 可很容易擴充 function,不再擔心污染
Array.prototype
- Ramda 使用
pipe()
,以 Function Pipeline 方式將資料逐步傳遞,它與compose()
的 Function Composition 剛好是一體兩面 - Point-free 也是 Ramda 一大特色,讓程式碼更精簡,可讀性更高