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