logo
  • 現在做什麼
  • 關於我

Kalan

文章分類

  • 前端
  • 開發筆記
  • 雜談
  • 年度回顧

快速連結

  • 現在做什麼
  • 關於我
  • 聯絡我
  • 職涯思考🔗

關注我

在福岡生活的開發者,分享軟體開發與日本生活的點點滴滴。

© 2025 Kalan Made with ❤️. All rights reserved.

淺談 ramda 中的幾個 API

由愷開愷開撰寫2017年8月25日 9:14
首頁/前端
💡

如果想問問題或單純回饋的話可以填寫表單唷

English日文

目錄

  1. 前言
  2. 直觀 functional programming
    1. propEq
    2. zipObj
    3. ifElse
    4. compose
    5. useWith
    6. converge
    7. identity
    8. tap
    9. pluck
    10. pick, pickBy, pickAll
    11. pathOr
    12. memoize
  3. 結論

前言

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 這兩個參數。

fn1

converge

這個函數跟上面的 useWith 有點類似,不過 converge 只接收一個參數。下面這張圖可以很直觀地看出這兩者的差別。

const numbers = [1, 2, 5, 8, 10]

const getRange = R.converge(substract, [getFirst, getLast])(numbers) // return 9

fn2

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 的世界。

← 淺談降維方法中的 PCA 與 t-SNE2017 年前端面試心得 →

如果覺得這篇文章對你有幫助的話,可以考慮下面的連結請我喝一杯 ☕ 可以讓我平凡的一天變得閃閃發光 ✨

☕Buy me a coffee

目錄

  1. 前言
  2. 直觀 functional programming
    1. propEq
    2. zipObj
    3. ifElse
    4. compose
    5. useWith
    6. converge
    7. identity
    8. tap
    9. pluck
    10. pick, pickBy, pickAll
    11. pathOr
    12. memoize
  3. 結論