Kalan's Blog

Software Engineer / Taiwanese / Life in Fukuoka

Current Theme light

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.

fn1

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

fn2

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.

Prev

Talking about PCA and T-Sne in Reduction Methods

Next

Front-end interview experience

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Hi, I'm Kai. I'm Taiwanese and moved to Japan in 2019 for work. Currently settled in Fukuoka. In addition to being familiar with frontend development, I also have experience in IoT, app development, backend, and electronics. Recently, I started playing electric guitar! Feel free to contact me via email for consultations or collaborations or music! I hope to connect with more people through this blog.