前言
ramda 是個相當好用的函式庫,如果聽過 lodash 或是 underscore 的話,可以將 ramda 想成 functional programming 的 lodash,他們的 API 有許多相似性,差別在於 ramda 本身有 FP 的功能,任何的 API 只要你沒有傳入參數,ramda 就會自動幫你做 curry,這提供了相當大的彈性。
比如說在 lodash 當中,API 常見的使用方式為:
_.map([1, 2, 3], n => n * 2) // [2, 4, 6]
而在 ramda 當中則是:
R.map(n => n * 2, [1, 2, 3]) // [2, 4, 6]
或者你可以這樣寫:
const times2 = R.map(n => n * 2) // return function
times2([1, 2, 3]) // [2, 4, 6]
這種方式讓我們不用被資料綁死,可以提高復用性。
如果你想要瞭解更多有關於 FP 的資訊,我推薦這篇文章:函數式程序設計為什麼至關重要
直觀 functional programming
- 一樣的 input 會輸出一樣的 output,不會受到外在狀態而改變結果
- 沒有副作用
###為什麼選擇 Ramda
ramda 的 API 相當多,大部分的 API 也都非常直觀,所以我們也不需要一一細談。不過 ramda 中有許多優秀的 API 或許可以幫助你減少開發上的複雜度,以下介紹幾個我覺得值得一看的 API
propEq
接受字串當作屬性,比對傳入的物件屬性值是否相等。
const obj = {
name: "kalan",
}
propEq("name", "kalan")(obj) // true
// 等價於
const propEq = (name, value) => obj => {
return obj[name] === value
}
zipObj
將傳入的參數壓縮為 object
。
R.zipObj(["id", "title"], ["2", "mytitle"])
/*
{
id: '2',
title: 'mytitle'
}
*/
ifElse
在做 branch 的邏輯時相當好用,你可能會問,幹嘛不直接用 if…else
就好?ramda 當中的 API 都是回傳函數,這代表你可以用 compose 來組裝其他 API。
compose
將函式組合,執行的順序是由內到外。可以把它聯想為國高中很常見的函數題 f(g(h(x)))
。所以要先計算 h(x)
的值,再依序下去。
const a = compose(
toInteger,
toCurrency("TWD"),
toUppercase
)("125000")
useWith
接收一個函數跟函數陣列。將資料丟入函數陣列運算出來的結果,再丟入第一個函數當中。
善用 useWith
可以幫助我們達到 point free 的效果。
const currencies = [
{ name: "TWD", shape: "$" },
{ name: "USD", shape: "$" },
{ name: "JPY", shape: "¥" },
{ name: "CAD", shape: "$" },
]
// without useWith
const getCurrency = (name, dic) => R.find(R.propEq("name", name), dic)
getCurrency("TWD", currencies) // $
// with useWith
const getCurrency = R.useWith(R.find, [R.propEq("name"), R.identity]) // 將第一個參數傳入 R.propEq,第二個參數傳入 R.identity,運算後的結果分別丟給 R.find 的第一與第二個參數。
getCurrency("TWD", currencies)
使用 useWith
後消除了 name
dic
這兩個參數。
converge
這個函數跟上面的 useWith
有點類似,不過 converge 只接收一個參數。下面這張圖可以很直觀地看出這兩者的差別。
const numbers = [1, 2, 5, 8, 10]
const getRange = R.converge(substract, [getFirst, getLast])(numbers) // return 9
identity
有點太直觀了......,反而不知道怎麼解釋。直接用程式碼解釋比較快。
const identify = arg => arg
至於為什麼要這樣做呢?有時你可能需要組織你的函數達到 chainable,這時 identity
這個函數就能夠派上用場。
tap
傳入參數給指定的函數,然後回傳值。這在 debug 或是串接其他第三方的時候相當好用。
tap(console.log)("hello world") // 傳入 hello world 給 console.log,並且回傳 hello world 這個值
上面的例子可能看不出來 tap
的用處,我們用搭配 compose
一起使用
const uploadToMedium = article => API.postArticle(article)
const notifyAdmin = article => API.notify(article, subscribers)
const log = article => Logger.log(article)
const preprocessArticle = article => article.toLowerCase()
const publishPostFlow = article =>
compose(
preprocessArticle,
R.tap(uploadToMedium),
R.tap(notifyAdmin),
R.tap(log)
)
publishPostFlow(article)
這樣一來我們可以很方便地串接其他服務,而不必再撰寫類似 return article
這樣的程式碼,一方面可以減少樣板代碼的麻煩,一方面可以減少出錯的機率。
pluck
跟字面上的意思差不多,將指定參數的 value 摘取下來變成新的值。在巢狀物件取值時很有幫助,例如:
const data = [
{
id: "1",
content: "content...",
},
{
id: "2",
content: "content...",
},
{
id: "3",
content: "content...",
},
]
const getIds = R.pluck("id", data) // return ['1','2','3']
pick, pickBy, pickAll
實務上我們可能不會用到物件當中所有的屬性,可能只要取出一些屬性來使用而已,這三個 pick
系列的函數可以很方便做到這件事:
const data = {
url: "https://api.github.com/repos/example/magazine/issues/1",
repository_url: "https://api.github.com/repos/example/magazine",
labels_url:
"https://api.github.com/repos/example/magazine/issues/1/labels{/name}",
comments_url:
"https://api.github.com/repos/example/magazine/issues/1/comments",
events_url: "https://api.github.com/repos/example/magazine/issues/1/events",
html_url: "https://github.com/example/magazine/issues/1",
id: 252372781,
number: 1,
title: "test issue",
}
R.pick(["url", "repository_url", "id", "name"], data) // 回傳這三個屬性的值,如果找不到此屬性直接忽略
R.pickAll(["url", "repository_url", "name"], data) // 回傳值如果屬性存在,沒有的話會回傳 undefined
const isURL = (value, key) => key.indexOf("_url") !== -1
R.pickBy(isURL, data) // 回傳任何屬性含有 _url
pathOr
前端在呼叫後端 API 時,回傳的 JSON 有時會有相當深層的結構,如果用一般的方式 a && a.b && a.b.c
的方式取值,不僅程式碼相當雜亂,而且一旦結構過深,就要寫更多的判斷式。
pathOr
接受陣列當作取值的順序,一旦取值回傳 undefined
就會回傳預設值。
const article = {
id: "116208916",
author: {
information: {
birthday: "1994-11-11",
name: "kalan",
},
subscribers_count: 1239,
},
content: {
title: "title",
body: "body",
},
}
如果後端回傳的欄位不齊全,很有可能造成錯誤發生,這個時候就能透過 pathOr
來做處理。這樣一來如果生日沒有值的話就會用「未提供生日」這個字串取代。
const getBirthday = R.pathOr(
["author", "information", "birthday"],
"未提供生日"
)
getBirthday(article)
memoize
在計算質數、階層等運算量比較大情景,為了不在每次求值時重新運算一次,可以用 memoize 函數快取已經運算過的結果。
結論
ramda 是個相當好用函式庫,這篇文章提出一些在一般操作比較少見的 API,不過 ramda 本身的 API 相當豐富,搭配 compose
你可以自由組裝自己的 function,並且利用以上提到方法來簡化你的函數。
如果你也喜歡這種程式碼風格,歡迎踏入 functional programming 的世界。