If you have any questions or feedback, pleasefill out this form
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
Introduction
Ramda is a highly useful library. If you're familiar with Lodash or Underscore, you can think of Ramda as the functional programming version of Lodash. Their APIs share many similarities, but the key difference is that Ramda supports FP features. Any API in Ramda will automatically perform currying if no parameters are passed, providing significant flexibility.
For example, in Lodash, a common way to use an API is:
_.map([1, 2, 3], n => n * 2) // [2, 4, 6]
In Ramda, it looks like this:
R.map(n => n * 2, [1, 2, 3]) // [2, 4, 6]
Or you can write it like this:
const times2 = R.map(n => n * 2) // return function
times2([1, 2, 3]) // [2, 4, 6]
This approach allows us to avoid being tightly bound to data, increasing reusability.
If you want to learn more about FP, I recommend this article: Why Functional Programming is Important
Intuitive Functional Programming
- The same input will always produce the same output, unaffected by external states.
- There are no side effects.
Why Choose Ramda
Ramda's API is extensive, and most of its functions are quite intuitive, so we don't need to dive into every single one. However, there are many excellent APIs that can help reduce complexity in your development. Below are a few APIs that I think are worth checking out.
propEq
Accepts a string as a property and checks if the value of that property in the passed object is equal.
const obj = {
name: "kalan",
}
propEq("name", "kalan")(obj) // true
// Equivalent to
const propEq = (name, value) => obj => {
return obj[name] === value
}
zipObj
Compresses the passed parameters into an object
.
R.zipObj(["id", "title"], ["2", "mytitle"])
/*
{
id: '2',
title: 'mytitle'
}
*/
ifElse
This is particularly useful for branching logic. You might wonder why not just use if…else
? The APIs in Ramda return functions, which means you can use compose to assemble other APIs.
compose
Combines functions, executing them from innermost to outermost. You can think of it like the common function problem from high school: f(g(h(x)))
. So you first calculate the value of h(x)
, then proceed outward.
const a = compose(
toInteger,
toCurrency("TWD"),
toUppercase
)("125000")
useWith
Accepts a function and an array of functions. It takes the data, applies the functions in the array to it, and then feeds the results into the first function.
Using useWith
can help us achieve a point-free style.
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]) // Passes the first argument to R.propEq and the second to R.identity, with the results going to the first and second arguments of R.find.
getCurrency("TWD", currencies)
Using useWith
eliminates the need for the name
and dic
parameters.
converge
This function is somewhat similar to the previous useWith
, but converge only takes one parameter. The following image illustrates the difference between the two.
const numbers = [1, 2, 5, 8, 10]
const getRange = R.converge(substract, [getFirst, getLast])(numbers) // returns 9
identity
This function is quite straightforward... so much so that I'm not sure how to explain it. It’s quicker to show with code.
const identify = arg => arg
Why would you do this? Sometimes, you need to structure your functions to achieve chainability, and that's where the identity
function comes in handy.
tap
Passes a parameter to a specified function and returns the value. This is especially useful in debugging or when integrating with third-party services.
tap(console.log)("hello world") // Passes "hello world" to console.log and returns "hello world"
In the above example, the usefulness of tap
might not be apparent, so let's see it in combination with 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)
This way, we can conveniently integrate other services without having to write boilerplate code like return article
repeatedly, reducing both the amount of boilerplate and the likelihood of errors.
pluck
This function does just as its name suggests: it extracts the values of the specified parameter into a new value. It is particularly useful when working with nested objects. For example:
const data = [
{
id: "1",
content: "content...",
},
{
id: "2",
content: "content...",
},
{
id: "3",
content: "content...",
},
]
const getIds = R.pluck("id", data) // returns ['1','2','3']
pick, pickBy, pickAll
In practice, we may not need to use all the properties of an object but might only want to extract a few. These three pick
functions can conveniently accomplish this:
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) // Returns the values of these three properties; if a property is not found, it is simply ignored.
R.pickAll(["url", "repository_url", "name"], data) // Returns values if properties exist; if not, it returns undefined.
const isURL = (value, key) => key.indexOf("_url") !== -1
R.pickBy(isURL, data) // Returns any properties that contain _url
pathOr
When front-end applications call back-end APIs, the returned JSON may have a deeply nested structure. Using the typical approach of a && a.b && a.b.c
can lead to messy code, and if the structure is too deep, it requires even more conditionals.
pathOr
accepts an array as the order for value retrieval. If any value retrieved returns undefined
, a default value is returned instead.
const article = {
id: "116208916",
author: {
information: {
birthday: "1994-11-11",
name: "kalan",
},
subscribers_count: 1239,
},
content: {
title: "title",
body: "body",
},
}
If the backend returns incomplete fields, errors can easily occur. In this case, pathOr
can handle it. If the birthday is absent, it will replace it with the string "Birthday not provided."
const getBirthday = R.pathOr(
["author", "information", "birthday"],
"未提供生日"
)
getBirthday(article)
memoize
In scenarios involving large calculations, such as finding prime numbers or computing factorials, you can use the memoize
function to cache previously computed results to avoid recalculating each time.
Conclusion
Ramda is an exceptionally useful library. This article has introduced some less commonly used APIs, but Ramda's API is quite extensive. With compose
, you can freely assemble your own functions and use the methods mentioned above to simplify your code.
If you enjoy this style of coding, welcome to the world of functional programming!
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee