Introduction
ramda is a very useful library. If you have heard of lodash or underscore, you can think of ramda as the lodash of functional programming. Their APIs have many similarities, but the difference is that ramda has built-in functional programming features. If you don't pass any parameters to the API, ramda will automatically curry them, providing great 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 would be:
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 way, we are not bound to specific data, which increases reusability.
If you want to learn more about functional programming, I recommend this article: Why Functional Programming is Essential
Intuitive Functional Programming
- Same input produces the same output, regardless of external state.
- No side effects.
Why Choose Ramda
Ramda has a wide range of APIs, most of which are very intuitive, so we don't need to discuss each one in detail. However, there are several excellent APIs in ramda that can help reduce development complexity. Here are a few worth mentioning:
propEq
Accepts a string as a property and checks if the value of the passed object's property 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
Useful for branching logic. You might ask, why not just use if...else
directly? The APIs in ramda return functions, which means you can use compose
to assemble other APIs.
compose
Combines functions, executing them from the inside out. You can think of it as a common function problem in high school, f(g(h(x)))
. So, you need to calculate the value of h(x)
first, and then proceed in order.
const a = compose(
toInteger,
toCurrency("TWD"),
toUppercase
)("125000")
useWith
Accepts a function and an array of functions. It takes the result obtained by applying the data to the array of functions and passes it to the first function.
Using useWith
can help us achieve 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]) // Pass the first parameter to R.propEq and the second parameter to R.identity, and then pass the computed results to the first and second parameters of R.find.
getCurrency("TWD", currencies)
Using useWith
eliminates the need for the name
and dic
parameters.
converge
This function is somewhat similar to useWith
, but converge
only accepts one parameter. The difference between the two can be seen intuitively in the following diagram.
const numbers = [1, 2, 5, 8, 10]
const getRange = R.converge(substract, [getFirst, getLast])(numbers) // return 9
identity
It's quite self-explanatory... it's hard to explain. Let's explain it directly with code.
const identify = arg => arg
So why do we do this? Sometimes, you may need to organize your functions to achieve chainability, and that's where the identity
function comes in handy.
tap
Passes the parameter to the specified function and returns the value. This is very useful for debugging or integrating with third-party services.
tap(console.log)("hello world") // Pass "hello world" to console.log and return "hello world"
The above example may not demonstrate the usefulness of tap
. Let's use it in conjunction 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 easily chain other services without having to write code like return article
. This not only reduces the hassle of boilerplate code but also reduces the chances of making mistakes.
pluck
Does what it says - extracts the values of the specified parameter and creates a new value. It is very helpful when extracting values from nested objects, for example:
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
In practice, we may not need all the properties of an object, but only a few specific ones. These three pick
functions make it convenient to achieve 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, ignoring any properties that are not found
R.pickAll(["url", "repository_url", "name"], data) // Returns the values if the properties exist, otherwise returns undefined
const isURL = (value, key) => key.indexOf("_url") !== -1
R.pickBy(isURL, data) // Returns any property that contains _url
pathOr
When calling backend APIs from the frontend, the returned JSON sometimes has a deep structure. If you use the conventional approach of a && a.b && a.b.c
to retrieve values, not only will the code be messy, but once the structure becomes too deep, you will need to write even more conditional statements.
pathOr
accepts an array as the retrieval order and returns a default value if the retrieved value is undefined
.
const article = {
id: "116208916",
author: {
information: {
birthday: "1994-11-11",
name: "kalan",
},
subscribers_count: 1239,
},
content: {
title: "title",
body: "body",
},
}
If the returned fields from the backend are not complete, it can often lead to errors. This is where pathOr
comes in handy. Now, if the birthday value is missing, it will be replaced with the string "Birthday Not Provided".
const getBirthday = R.pathOr(
["author", "information", "birthday"],
"Birthday Not Provided"
)
getBirthday(article)
memoize
In scenarios where calculations such as prime numbers or factorials require significant computational resources, in order to avoid recomputing the same values each time, you can use the memoize
function to cache the results.
Conclusion
Ramda is a very useful library. This article introduces some less commonly used APIs, but ramda itself has a rich API. With compose
, you can freely assemble your own functions and simplify them using the methods mentioned above.
If you also appreciate this coding style, welcome to the world of functional programming.