Ramda Guide

This is a guide to help get you mostly adequate1 with ramda.js, "a practical functional library for JavaScript programmers".

By Robert W. Pearce

Book Status

Updated: 2022-03-26

As you can see from the navigation links, this is a work in progress. I will post updates to the feed as new groups of content are available, you can subscribe for email updates on the website, or you can check the news page.

View the guide's source code.

Special Thanks

How to Use This Guide

The Content

This guide's content consists of chapters grouped into related sections. Each section has a theme that is based on the data we use as examples.

Here are some example data we'll work with:

  • Weather
  • The International Space Station's location
  • Earthquakes
  • Internationalization (i18n)
  • Observatories

Many examples include explanations outside and inside code blocks; the latter are done via code comments.

Advanced Content

Content that I deemed to be advanced and perhaps not immediately relevant for newcomers is marked as "Advanced" in chapters where the advanced material is contained.

Meta

This guide is generated via mdBook. Here are some quick tips:

  • Open & close the sidebar by mashing on the three horizontal lines (hamburger) icon above
  • Change the theme by mashing on the paint brush icon above
  • Do a text search through the entire guide by mashing on the magnifying glass icon above
  • Not a fan of reading on the web? Mash the printer icon above and print it or save to PDF
  • Want to link to a specific heading? Each heading is a link you can mash, and then you can share the URL with the world

Disclaimer / Personal Note

Even though I've worked with Ramda since 2016, programmed professionally since 2011, and taught programming for a while, I don't have a computer science degree (yet), and I am not a Category Theory expert. The opinions in this guide stem from my experience learning functional programming concepts through a number of languages and making heaps of mistakes along the way. There are also a lot of people who invested in me, and I am thankful for their guidance along the path.

If you find mistakes or hold different opinions, feel free to open an issue, and we can publicly discuss things there.

I'll never stop learning and being better than I was yesterday, and I hope that perspective provides you, dear reader, some value as you embark on your functional programming journey.

What is Ramda?

Ramda.js is a JS library of helper functions that have some cool principles baked into every function:

  • never mutate the user's data
  • always provide the same output given the same input
  • allow users to build new functions from old ones by not supplying all of a function's parameters

These ideas — regardless of what library or even language we use — allow us to write safe, extendable code, and the ideas are explored further in the Core Ramda Ideas section that starts with the "Same Input → Same Output" chapter.

Suggested Prerequisite Knowledge

The presentation of concepts in this guide assumes the reader is familiar with some JS fundamentals, so take a look below and see how comfortable you are with the concepts.

If you're not comfortable with them, each section has a link to more information about its topic, and if that's not enough, here are some resources for learning more JS:

Arrow Functions

MDN: Arrow Functions

const add = (a, b) => a + b

add(4, 5) // 9

Nested Functions

MDN: Nested Functions and Closures

const addExpr = a => b => a + b

addExpr(4)(5) // 9

// or

function addFn(a) {
  return function (b) {
    return a + b
  }
}

addFn(4)(5) // 9

Passing Functions as Arguments (Callbacks)

MDN: First-Class Function

const log = x => console.log(x)

[1, 2, 3].forEach(log)
// 1
// 2
// 3

Map, Filter, & Reduce

MDN: map, filter, and reduce

[1, 2, 3].map(x => x * 2)                      // [2, 4, 6]

[1, 2, 3].filter(x => x % 2 !== 0)             // [1, 3]

[1, 2, 3].reduce((sum, item) => sum + item, 0) // 6

Calling Functions

MDN: Calling Functions

const add5    = x => x + 5
const times10 = x => x * 10
const div2    = x => x / 2

div2(times10(add5(15))) // 100

Related Projects

Ramda is great for teams who are stepping into the functional programming in JS world and want to dip their toes in the water. Once you've gotten comfortable with writing code in a functional style, consider checking out these projects, as well, to take it to the next level.

Each of these projects below include some amount of algebraic data types (ADTs) for safety and expressiveness.

It should also be said that there are numerous great projects out there, so please open an issue if you'd like to see others listed below.

Sanctuary

Sanctuary is a successor to Ramda that is quite a bit stricter and more likely to be unfamiliar to web developers, for it feels more like the ML language family.

It's goal is to provide refuge from unsafe JS. Check out its section on Ramda to read about the differences.

Crocks

Crocks, like Sanctuary, has a bit of overlap with Ramda's functions, but it goes much deeper than Ramda by providing a variety of ADTs and functions for working with them.

It is worth checking out at least for its Async ADT (goodbye, Promise)!

Folktale

Folktale, like Sanctuary and Crocks, includes some ADTs and other useful functions for working with a functional programming style in JS.

Including Ramda in A Project

There are instructions on Ramda's homepage detailing how to install Ramda, and deno.land's Ramda page has instructions for deno.

If you're building for the frontend and are using a build tool that has tree-shaking or dead-code elimination, then here is how you should import functions from Ramda:

import { compose, lensProp, map, over } from 'ramda'

However, if you do not have a build tool that does tree-shaking, you may want to import directly from the files you use to avoid importing the entire Ramda library when you only want to use a few functions. The two options with v0.27.1 are ESModules- and CommonJS-based.

// ESModules
import compose from 'ramda/es/compose'
import lensProp from 'ramda/es/lensProp'

// CommonJS
const compose = require('ramda/src/compose')
const lensProp = require('ramda/src/lensProp')

Documentation, Source Code, REPL, & Cookbook

Ramda's documentation is the place to go when you are looking for a function to use, trying to remember how to use a function, want to view a function's source code on GitHub, or open it in the Ramda REPL.

The Ramda REPL is something I used all the time when learning Ramda, for all the functions are automatically loaded in that environment for you to use. You can even get a link to your current state in the REPL and share it with others!

Some official resources of additional note are Ramda's GitHub wiki and the Ramda Cookbook. The wiki contains numerous resources hand-picked by the Ramda folks, and the Cookbook is chock-a-block with Ramda recipes (helpful functions built with Ramda functions).

Community Resources

There are a lot of great articles and resources out there, and Iain's list (see below) should cover most of your needs. If you'd like to see something on this page, feel free to open an issue, and we can discuss getting it added!

  • Iain Freestone's Ramda: My library of resources is an impressive compendium of blog posts, videos, podcasts, REPLs, Ramda-related libraries / tools, examples, and more.
  • Dave Sancho's Learn Ramda project is fun and helpful, for it uses dropdown selects and English sentences to help you find a function based on your needs, and it tells you how to use it.
  • ramda-adjunct bills itself as "the most popular and most comprehensive set of functional utilities for use with Ramda, providing a variety of useful, well tested functions with excellent documentation". It is filled with useful helper functions that make working with Ramda even easier.
  • (Shameless plug) My Ramda Chops series has a few of articles that might help you with currying, function composition, map, filter, & reduce, as well as a lengthy article on implementing your own functional programming-style map function. We'll be covering most of that content here, as well, as it was the inspiration for this guide!

Where We're Going

Meme of Doc Brown from Back to the Future in a Delorean with the text, Classes? Where we're going, we don't need classes

Buckle up! We are about to begin our tour of Ramda.

We will start small and practical, getting a feel for importing and using ramda.

After some practice, we'll take a break and cover Ramda's core ideas. In this section, we'll answer the burning "D-I-Why???" questions you will surely have.

Continuing on the path to enlightenment, we'll dive deep into various Ramda functions and concepts, melt our minds a little bit, and come out the other end empowered by knowledge and confident via practice.

By the end of this guide, you will be able to:

  • understand a majority of Ramda's most valuable functions
  • use functional programming to start solving problems functionally at work
  • guide others
  • continue on your functional programming path to enlightenment, wherever it may lead

Converting Temperature Units

Life is far too important a thing ever to talk seriously about.

Oscar Wilde, Lady Windermere's Fan

To kick off our Ramda journey, we're going to do something ridiculous: transform very clear temperature conversion functions that use JS math operators to use only functions for the operations!

Sometimes, when we forego the obvious and choose to approach problems in different ways, interesting patterns may emerge that can expand our understanding.

Let's get introduced to some perfectly fine conversion functions — one of which we are going to rip apart and make anew.

function celsiusToFahrenheit(celsius) {
  return celsius * (9 / 5) + 32
}

function fahrenheitToCelsius(fahrenheit) {
  return 5 / 9 * (fahrenheit - 32)
}

function easyCelsiusToFahrenheit(celsius) {
  return celsius * 2 + 30
}

function easyFahrenheitToCelsius(fahrenheit) {
  return (fahrenheit - 30) / 2
}

While the celsiusToFahrenheit and fahrenheitToCelsius functions are exact formulas, they're not practical for everyday use. I've lived in the UK and New Zealand, and since I'm married to a Kiwi, I need to easily convert between Celsius and Fahrenheit. While not exact, the easyCelsiusToFahrenheit and easyFahrenheitToCelsius formulas are easy to do in one's head and are close enough to the real values.

We are going to single out celsiusToFahrenheit for this extended example.

function celsiusToFahrenheit(celsius) {
  return celsius * (9 / 5) + 32
}

Enter the Ramda

In it, we:

  1. multiply the Celsius value by the result of 9 / 5
  2. add 32 to the result of the prior step(s)

Before we go further, let's first convert it to an arrow function expression, for doing so will open some interesting doors.

const celsiusToFahrenheit = celsius =>
  celsius * (9 / 5) + 32

Next, let's get Ramda pulled into the picture.

Ramda has a number of math functions, namely multiply, divide, and add that we can leverage in place of *, /, and +. Each function takes two arguments, and each function will wait to evaluate itself until you provide all the arguments. Check this out:

add(1, 2)     // 3
add(1)(2)     // 3
add()(1, 2)   // 3
add()(1)()(2) // 3

This is indeed weird, and we'll cover this fully in the "Core Ramda Ideas" section.

For now, let's import those and use them!

import { add, divide, multiply } from 'ramda'

const celsiusToFahrenheit = celsius =>
  add(multiply(celsius, divide(9, 5)), 32)

celsiusToFahrenheit(100) // 212

Woah, woah, woah! What's going on here?!

It looks like we're...

  1. adding 32 to the result of
  2. multiplying the Celsius value by the result of dividing 9 by 5

That's the same process we did before, but it's merely explained differently!

With addition and multiplication, there's something called the commutative law that states we can provide the arguments to an addition and multiplication operation in any order. Let's leverage this law in order to move our variable, celsius, further toward the edge of our function to judge how it feels.

// this is what we're starting with
add(multiply(celsius, divide(9, 5)), 32)

// first, swap `celsius` and `divide(9/5)`
add(multiply(divide(9, 5), celsius), 32)
//               ^------------^

// next, swap the multiplication and `32`
add(32, multiply(divide(9, 5), celsius))
//  ^------^

// the result
const celsiusToFahrenheit = celsius =>
  add(32, multiply(divide(9, 5), celsius))

Interesting! Do you see it yet? The forwarding of a result from function to function? Let's look at this another way:

const celsiusToFahrenheit = celsius => {
  const multiplied = multiply(divide(9, 5), celsius)
  const added = add(32, multiplied)

  return added
}

We provide celsius as the second argument to multiply, then we provide the result of that as the second argument to add. We're simply forwarding the evaluated result of a computation to another function; kind of like passing an electric guitar's signal through a few effects pedals and then out the amplifier.

What if we had a cleaner way to link these functions together so we can easily understand what celsiusToFahrenheit is composed of and then provide the data at the end?

It's time to take this first lesson into overdrive.

A Taste of Composition

We need a way of passing the result of calling one function to another function and having that run. It'd be easier if we could abstract an API... let's try that.

// this is essentially what we have
// with our celsiusToFahrenheit
f2(f1(value))

// but we want something like this;
// let's call it `link` because
// we're linking functions together
link(f2, f1)(value)

With that desired outcome in mind, let's try to write link!

const link = (f2, f1) => value =>
  f2(f1(value))

Ha! We're still doing the difficult to follow f2(f1(value)), but now we can use this like link(f2, f1)(value).

Circling back to celsiusToFahrenheit, let's try to use this link abstraction:

// before
const celsiusToFahrenheit = celsius =>
  add(32, multiply(divide(9, 5), celsius))

// after
const celsiusToFahrenheit = celsius =>
  link(add(32), multiply(divide(9, 5)))(celsius)

celsiusToFahrenheit(100) // 212

Nice! We can now do a little less inside-out reading. But something doesn't feel quite right... Why are we accepting the argument celsius in our celsiusToFahrenheit function only to turn right back around and call link() with the celsius value? Do we need it?

Nope.

// before
const celsiusToFahrenheit = celsius =>
  link(add(32), multiply(divide(9, 5)))(celsius)

// after
const celsiusToFahrenheit =
  link(add(32), multiply(divide(9, 5)))

celsiusToFahrenheit(100) // 212

You may be wondering why link reads right to left. Two short answers are:

  1. Mathematics writes f(x) and not (x)f
  2. Evaluation is done from right to left (inside -> outside), so we are right-associative

However, let me ease your worried mind and make a linkL (L for "left") function for us to use:

const linkL = (f1, f2) => value =>
  f2(f1(value))

And when we compare that to the original function, we realize that we've come nearly full circle but with a whole new perspective:

// where we started
const celsiusToFahrenheit = celsius =>
  celsius * (9 / 5) + 32
//        ^    ^    ^
//  multiply   |    |
//           divide |
//                 add

// where we ended up
const celsiusToFahrenheit =
  linkL(multiply(divide(9, 5)), add(32))

Ramda provides a few functions, compose (or o) and pipe that do the link and linkL work for us!

import {
  add,
  compose,
  divide,
  multiply,
  pipe,
} from 'ramda'

// `compose` and `o` are very similar
const celsiusToFahrenheit =
  compose(add(32), multiply(divide(9, 5)))

// `pipe`
const celsiusToFahrenheit =
  pipe(multiply(divide(9, 5)), add(32))

We'll cover function composition a bit more in the "Core Ramda Ideas" section.

Your Turn

Can you convert the remaining temperature conversion functions to use Ramda functions? Give them a try in a pre-loaded Ramda REPL.

Here they are again, in case that link doesn't work:

function fahrenheitToCelsius(fahrenheit) {
  return 5 / 9 * (fahrenheit - 32)
}

function easyCelsiusToFahrenheit(celsius) {
  return celsius * 2 + 30
}

function easyFahrenheitToCelsius(fahrenheit) {
  return (fahrenheit - 30) / 2
}

const result = () => ({
  '212F = 100C': fahrenheitToCelsius(212),
  '25C ≈ 80F': easyCelsiusToFahrenheit(25),
  '60F ≈ 15C': easyFahrenheitToCelsius(60),
})

result()

When you're done, compare them against my solutions!

Expand this to see my solutions if the link doesn't work
//function fahrenheitToCelsius(fahrenheit) {
//  return 5 / 9 * (fahrenheit - 32)
//}

// This is the best I could do before I had to cheat... see below!
//const fahrenheitToCelsius = fahrenheit =>
//  multiply(divide(5, 9), subtract(fahrenheit, 32))

// If you're feeling clever, check this out:
// https://ramdajs.com/docs/#__
const fahrenheitToCelsius =
  compose(multiply(divide(5, 9)), subtract(__, 32))

// ===============================================================

//function easyCelsiusToFahrenheit(celsius) {
//  return celsius * 2 + 30
//}

// Step 1:
//const easyCelsiusToFahrenheit = celsius =>
//  add(30, multiply(2, celsius))

// Step 2:
const easyCelsiusToFahrenheit =
  compose(add(30), multiply(2))

// ===============================================================

//function easyFahrenheitToCelsius(fahrenheit) {
//  return (fahrenheit - 30) / 2
//}

// This is the best I could do before I had to cheat... see below!
//const easyFahrenheitToCelsius = fahrenheit =>
//  divide(subtract(fahrenheit, 30), 2)

// If you're feeling clever, check this out:
// https://ramdajs.com/docs/#__
const easyFahrenheitToCelsius =
  compose(divide(__, 2), subtract(__, 30))

// ===============================================================

const result = () => ({
  '212F = 100C': fahrenheitToCelsius(212),
  '25C ≈ 80F': easyCelsiusToFahrenheit(25),
  '60F ≈ 15C': easyFahrenheitToCelsius(60),
})

result()

Wrapping Up

This turned out to be far from a gentle introduction!

We started with some addition, division, and multiplication to convert temperature values, and we ended up walking backwards into the heart of functional programming.

Way to go!

Comparing Two Temperatures

It doesn't matter what temperature the room is — it's always room temperature.

Steven Wright

We're at work, and we are tasked with writing two functions that compare any two temperatures:

  • One that gets the minimum value
  • One that gets the maximum value

We're not sure why they don't use Math.min and Math.max themselves, but we do what they ask.

const minTemperature = (a, b) => Math.min(a, b)
const maxTemperature = (a, b) => Math.max(a, b)

minTemperature(100, 200) // 100
maxTemperature(100, 200) // 200

Once we share that solution, we are reminded that task is to compare any two temperatures; for example, 100, 200, '50F', '25C'.

Dangit... now we look silly. Okay, let's try this again to make it handle strings:

const minTemperature = (a, b) => a > b ? b : a
const maxTemperature = (a, b) => a < b ? b : a

minTemperature(100, 200)       // 100
maxTemperature(100, 200)       // 200
minTemperature('200F', '100F') // '100F'
maxTemperature('200F', '100F') // '200F'

Then we realize that Ramda has min and max functions, so we use those and pat ourselves on the back:

import { max, min } from 'ramda'

min(100, 200)       // 100
max(100, 200)       // 200
min('200F', '100F') // '100F'
max('200F', '100F') // '200F'

We go back and tell our team to use Ramda's functions, but we're wrong again, and we realize that we probably should have asked for a thorough list of inputs.

But what could we have missed?

Compare any two temperatures.

Oh, no... They might not be part of the same measurement system! This means that '50F' could be compared with '15C'! Luckily, we're told that we don't have to worry about Kelvin, Rankine, nor Réaumur; we only care about Celsius and Fahrenheit. There will also never be any unit-less numbers, so we can ignore that case.

This problem just took on a whole new dimension!

Good news: in the last chapter, we wrote our celsiusToFahrenheit and fahrenheitToCelsius functions, so we have those functions we can use to do those calculations for us.

Let's try again:

import { max, min } from 'ramda'

const celsiusToFahrenheit =
  compose(add(32), multiply(divide(9, 5)))

const minTemperature = (a, b) => {
  const unitA = a.slice(-1) // get the last character, like C or F
  const unitB = b.slice(-1)
  const floatA = parseFloat(a)
  const floatB = parseFloat(b)

  // same temperature type
  if (unitA === unitB) {
    const maxT = min(floatA, floatB)
    return maxT === floatA ? a : b
  }

  // a is Fahrenheit but b is Celsius
  if (unitA === 'F') {
    const floatBAsF = celsiusToFahrenheit(floatB)
    const minF = min(floatA, floatBAsF)

    return minF === floatA ? a : b
  }

  // a is Celsius but b is Fahrenheit
  const floatAAsF = celsiusToFahrenheit(floatA)
  const minF = min(floatAAsF, floatB)

  return minF === floatB ? b : a
}

const maxTemperature = (a, b) => {
  const unitA = a.slice(-1) // get the last character, like C or F
  const unitB = b.slice(-1)
  const floatA = parseFloat(a)
  const floatB = parseFloat(b)

  // same temperature type
  if (unitA === unitB) {
    const maxT = max(floatA, floatB)
    return maxT === floatA ? a : b
  }

  // a is Fahrenheit but b is Celsius
  if (unitA === 'F') {
    const floatBAsF = celsiusToFahrenheit(floatB)
    const maxF = max(floatA, floatBAsF)

    return maxF === floatA ? a : b
  }

  // a is Celsius but b is Fahrenheit
  const floatAAsF = celsiusToFahrenheit(floatA)
  const maxF = max(floatAAsF, floatB)

  return maxF === floatB ? b : a
}

minTemperature('200F', '100F') // '100F'
maxTemperature('200F', '100F') // '200F'
minTemperature('50F', '25C')   // '50F'
maxTemperature('50F', '25C')   // '25C'

(View this large min/max temperature example in the Ramda REPL)

Goodness gracious! While we could extract some common functionality into many small functions, there must be a simpler way. After all, isn't functional programming supposed to help us simplify things?

(Hint: yes!)

Ramda has minBy and maxBy functions that will compare two values after they have been transformed by some transformation function. Here's their example from the docs:

import { maxBy } from 'ramda'

const square = n => n * n
maxBy(square, -3, 2) // -3

In this example, maxBy will call square with -3 and 2, and it will then compare each of those results. Whatever value has the largest result after being applied to square will be the returned value. Here, -3 * -3 is 9, whereas 2 * 2 is 4, so since 9 > 4, -3 is our result.

Let's refactor our functions:

import { maxBy, minBy } from 'ramda'

const asF = x => {
  if (x.slice(-1) === 'C') {
    return celsiusToFahrenheit(parseFloat(x))
  }

  return x
}

const minTemperature = (a, b) => minBy(asF, a, b)
const maxTemperature = (a, b) => maxBy(asF, a, b)

minTemperature('200F', '100F') // '100F'
maxTemperature('200F', '100F') // '200F'
minTemperature('50F', '25C')   // '50F'
maxTemperature('50F', '25C')   // '25C'
maxTemperature('50C', '25C')   // '50C'

(View this v1 minBy/maxBy temperature example in the Ramda REPL)

By casting all temperatures as a single unit (I chose Fahrenheit here), we can compare any temperatures and get back their original values! 25C is indeed hotter than 50F!

But wait – there's more.

Just like we noticed in the last chapter, our minTemperature and maxTemperature functions are taking in (a, b) and are merely forwarding those arguments on. If you recall, Ramda doesn't care when you provide arguments, so check this out...

// before
const minTemperature = (a, b) => minBy(asF, a, b)
const maxTemperature = (a, b) => maxBy(asF, a, b)

// after
const minTemperature = minBy(asF)
const maxTemperature = maxBy(asF)

(View this v2 minBy/maxBy temperature example in the Ramda REPL)

Oh, and one more thing; here's a preview of some upcoming chapters' content retroactively applied to making our asF function cleaner:

// before
const asF = x => {
  if (x.slice(-1) === 'C') {
    return celsiusToFahrenheit(parseFloat(x))
  }

  return x
}

// after

// Celsius     = String // "100C"
// Fahrenheit  = String // "100F"
// Temperature = Celsius | Fahrenheit

// isC :: Temperature -> Bool
const isC = compose(equals('C'), slice(-1))

// stringCToF :: Celsius -> Number
const stringCToF = compose(celsiusToFahrenheit, parseFloat)

// asF :: Temperature -> Fahrenheit
const asF = when(isC, stringCToF)

And here is what all of the new functions together could look like!

// Celsius     = String // "100C"
// Fahrenheit  = String // "100F"
// Temperature = Celsius | Fahrenheit

// isC :: Temperature -> Bool
const isC = compose(equals('C'), slice(-1))

// stringCToF :: Celsius -> Number
const stringCToF = compose(celsiusToFahrenheit, parseFloat)

// asF :: Temperature -> Fahrenheit
const asF = when(isC, stringCToF)

// minTemperature :: Temperature -> Temperature -> Temperature
const minTemperature = minBy(asF)

// maxTemperature :: Temperature -> Temperature -> Temperature
const maxTemperature = maxBy(asF)

(View this v3 minBy/maxBy temperature example in the Ramda REPL)

Cool, right?

Fear not! We'll cover when in the "Unshakeable Logic" section, slice in the "All About ['L','i','s','t','s']" section, and the pseudo-type signatures in the "Core Ramda Ideas" section, but you should know the pseudo-types are optional, not exact, and aren't a substitute for tools like jsdoc.

Wrapping Up

The person who requested these functions is going to be blown away!

What started as a simple "which temperature is smaller or larger?" question turned out to be an exercise in asking good questions about requirements up front.

We also walked through implementing a naïve solution and refactored it to a more elegant one by leveraging Ramda and functional programming.

Our Data: The ISS' Location

In life, and in the universe, it's always best to keep looking up.

Neil deGrasse Tyson

In this section, we're going to look up object properties in all sorts of ways, and what better data could we ask for than data for something that is orbiting our world?

The Awesome JSON Datasets repository has a NASA section that links to Open Notify's ISS Now and How Many People Are In Space Right Now? endpoints.

Here is an example response that shares where on Earth the International Space Station is right now:

{
  "message": "success",
  "timestamp": 1617930803,
  "iss_position": {
    "latitude": "27.7270",
    "longitude": "133.2581"
  }
}

And here is an example of "How Many People Are In Space Right Now?" as of 2021-04-08:

{
  "message": "success",
  "number": 7,
  "people": [
    {
      "craft": "ISS",
      "name": "Sergey Ryzhikov"
    },
    {
      "craft": "ISS",
      "name": "Kate Rubins"
    },
    {
      "craft": "ISS",
      "name": "Sergey Kud-Sverchkov"
    },
    {
      "craft": "ISS",
      "name": "Mike Hopkins"
    },
    {
      "craft": "ISS",
      "name": "Victor Glover"
    },
    {
      "craft": "ISS",
      "name": "Shannon Walker"
    },
    {
      "craft": "ISS",
      "name": "Soichi Noguchi"
    }
  ]
}

We'll use these simple datasets to show off a number of very helpful Ramda functions that deal with looking up object properties.

Reading Shallow Object Properties

Too much gravity argues a shallow mind.

Johann Kaspar Lavater

Let's dive (but not too deep!) into pulling out data at a single level from objects!

Outline:

In case you missed it, we'll be using some ISS and astronaut data as our datasets.

ISS' Current Location
{
  "message": "success",
  "timestamp": 1617930803,
  "iss_position": {
    "latitude": "27.7270",
    "longitude": "133.2581"
  }
}
"How Many People Are In Space Right Now?"
{
  "message": "success",
  "number": 7,
  "people": [
    {
      "craft": "ISS",
      "name": "Sergey Ryzhikov"
    },
    {
      "craft": "ISS",
      "name": "Kate Rubins"
    },
    {
      "craft": "ISS",
      "name": "Sergey Kud-Sverchkov"
    },
    {
      "craft": "ISS",
      "name": "Mike Hopkins"
    },
    {
      "craft": "ISS",
      "name": "Victor Glover"
    },
    {
      "craft": "ISS",
      "name": "Shannon Walker"
    },
    {
      "craft": "ISS",
      "name": "Soichi Noguchi"
    }
  ]
}

We will assume we're storing those objects as variables named iss and astros.

prop

In vanilla JS, we can get the timestamp key off the ISS object like this:

iss.timestamp

// or

iss['timestamp']

If that's the only functionality we'll ever need, then that's great! We can stop here.

But what if we want to do more? For example,

  1. get the timestamp
  2. multiply the timestamp by 1000 to convert it to milliseconds
  3. convert it to a Date string

Our first attempt might be to do this inline:

new Date(iss.timestamp * 1000)
// "Sat Apr 10 2021 15:06:50 GMT+0000 (Coordinated Universal Time)"

Then we realize that we want to do this for many different ISS location objects, so we write a function:

const issTimeToDate = data =>
  new Date(data.timestamp * 1000)

issTimeToDate(iss)
// "Sat Apr 10 2021 15:06:50 GMT+0000 (Coordinated Universal Time)"

It is totally acceptable to stop at this point.

Maybe we should, but we don't.

Squinting at that code a little harder, we notice that there are three transformations happening:

  1. from the ISS object, we get the shallow property, timestamp
  2. we multiply that value by 1000
  3. we instantiate a new Date with the prior result

And we also notice that if data is ever undefined or null (or anything that isn't an instance of Object), we're going to have a problem!

issTimeToDate(null)
// Uncaught TypeError: can't access property "timestamp", data is null

As you may recall from our "First Taste of Composition", if we extract each operation into its own function, there is a way we can "link" these fuctions together: compose!

// Here we create a reusable function that
// receives an object property, then returns
// a function that accepts an object, then
// tries to access that property on the object
const getProp = property => data => {
  if (data instanceof Object) {
    return data[property]
  }
}

const toMilliseconds = n => n * 1000
const toDate         = n => new Date(n)

const issTimeToDate =
  compose(toDate, toMilliseconds, getProp('timestamp'))

issTimeToDate(iss)
// "Sat Apr 10 2021 15:06:50 GMT+0000 (Coordinated Universal Time)"

View this getProp with compose example in the Ramda REPL.

While this doesn't handle all edge cases, at least passing null to issTimeToDate will give us an Invalid Date message.

That getProp function looks like it's fairly generic, but could it handle an Array? Could we leverage it to figure out who the first astronaut is in the astros.people list?

compose(getProp(0), getProp('people'))(astros)
// {
//   "craft": "ISS",
//   "name": "Sergey Ryzhikov"
// }

// which can be refactored and reused
// with any group of astronauts

const getFirstAstro =
  compose(getProp(0), getProp('people'))

getFirstAstro(astros)

// and if you really want to get some
// reusable functions

const getPeople     = getProp('people')
const getFirst      = getProp(0)
const getFirstAstro = compose(getFirst, getPeople)

getFirstAstro(astros)
// {
//   "craft": "ISS",
//   "name": "Sergey Ryzhikov"
// }

View this getFirstAstro example in the Ramda REPL.

It can handle an Array! Why?

[] instanceof Object // true

An Array of [5, 10, 15] is an Object instance whose keys are Array indices!

Array(3) {
  0: 5,
  1: 10,
  2: 15,
  length: 3
}

This means getProp(1)([5, 10, 15]) === 10. Neat!

As you probably guessed by now, Ramda has a prop function that does what our getProp function does (and more), and there are a couple of other functions we could pull in to help us. Let's refactor!

The ISS example:

import { compose, multiply, prop } from 'ramda'

const getTimestamp   = prop('timestamp')
const toMilliseconds = multiply(1000)
const toDate         = n => new Date(n)

const issTimeToDate =
  compose(toDate, toMilliseconds, getTimestamp)

issTimeToDate(iss)
// "Sat Apr 10 2021 15:06:50 GMT+0000 (Coordinated Universal Time)"

View this final issTimeToDate example in the Ramda REPL.

Finding the first astronaut example:

import { compose, head, prop } from 'ramda'

const getPeople     = prop('people')
const getFirst      = prop(0)
const getFirstAstro = compose(getFirst, getPeople)

getFirstAstro(astros)
// {
//   "craft": "ISS",
//   "name": "Sergey Ryzhikov"
// }

View this final getFirstAstro example in the Ramda REPL.

But beware: if the property doesn't exist, or it returns null or undefined, then your composed functions will also need to be able to handle those scenarios or risk throwing an error.

propOr

When dealing with code or data that can give us back null or undefined values, we often try to be safe. Consider this code trying to access the ISS data:

iss.iss_position.latitude

That doesn't look so bad, does it? But what happens if the API endpoint changes its response on us or is having a bad day? Consider what would happen if the endpoint returned an empty object, {}, and that was our iss value:

iss.iss_position.latitude
// Uncaught TypeError: can't access property "latitude", iss.iss_position is undefined

Okay, so let's make that a little more fault tolerant:

(iss.iss_position || {}).latitude
// undefined

At least we're not throwing an error right now... but what if the response is null instead of {}?

(iss.iss_position || {}).latitude
// Uncaught TypeError: null is not a function

Argh! We need to do the same thing for iss.iss_position:

((iss || {}).iss_position || {}).latitude
// undefined

We're now able to handle these edge cases, but imagine two new requirements arise:

  1. we need to have the latitude fall back to a previous latitude value if the current one is unattainable
  2. the value needs to be a floating point number and not a string
const prevLatitude = '-44.7894'
parseFloat(((iss || {}).iss_position || {}).latitude || prevLatitude)
// 27.7270

This is starting to get messy, so we think breaking it into variables will help:

const prevLatitude = '-44.7894'
const issObj = iss || {}
const issPosition = issObj.iss_position || {}
const issLatitude = issPosition.latitude || prevLatitude
parseFloat(issLatitude)
// 27.7270

Not bad, but there must be a cleaner way to do this!

The propOr function, whose signature is a → String → Object → a, takes the following arguments:

  1. a fallback value of some type a
  2. a property name as a String
  3. some Object to look the property up on

and then returns some value which is also of some type a.

Let's convert our variables to use propOr and walk things back from there:

const prevLatitude = '-44.7894'
// no need for `issObj` anymore
const issPosition = propOr({}, 'iss_position', iss)
const issLatitude = propOr(prevLatitude, 'latitude', issPosition)
parseFloat(issLatitude)
// 27.7270

While we removed the issObj line of code, it looks like we have almost the same amount of code. The difference, though, is what we can now do with this.

Do you see how these lines all basically use the return value from the line above? We've got a composition on our hands again!

const prevLatitude = '-44.7894'

const latitudeOrPrevLatitude =
  compose(
    parseFloat,
    propOr(prevLatitude, 'latitude'),
    propOr({}, 'iss_position')
  )

latitudeOrPrevLatitude(iss)       // 27.727
latitudeOrPrevLatitude({})        // -44.7894
latitudeOrPrevLatitude(null)      // -44.7894
latitudeOrPrevLatitude(undefined) // -44.7894

View this latitudeOrPrevLatitude example in the Ramda REPL.

Let's quickly walk through what passing undefined would have each line result in.

const latitudeOrPrevLatitude =
  compose(
    parseFloat,                       // 3. converts string to -44.7894
    propOr(prevLatitude, 'latitude'), // 2. falls back to "-44.7894"
    propOr({}, 'iss_position')        // 1. falls back to `{}`
  )

If you have good, generic fallbacks, you can then take it a step further and simplify:

const safeIssPosition = propOr({}, 'iss_position')
const safeLatitude = propOr(prevLatitude, 'latitude')

const latitudeOrPrevLatitude =
  compose(parseFloat, safeLatitude, safeIssPosition)

props

There may be times where we'd like to apply prop's functionality to more than a single prop; naturally, Ramda calls this props.

Our iss object has the following keys:

  • message
  • timestamp
  • iss_position

We've been told that we need to report on the success or failure of the API request, and we need the timestamp of when it happened in a format like "success @ 1617930803". In this case, we don't need the position of the ISS, so we can ignore it.

Without getting fancy, here's our first attempt:

// responseToStatus :: IssResponse -> String
const responseToStatus = ({ message, timestamp }) =>
  `${message} @ ${timestamp}`

This is great, and for this simple example, we could probably stop here; however, let's see if we can take this a few steps further.

Here's how we can use props to get the values of only message and timestamp:

props(['message', 'timestamp'], iss)
// ["success", 1617930803]

We can then join these together and accomplish our goal with .join(' @ ')

props(['message', 'timestamp'], iss).join(' @ ')
// "success @ 1617930803"

This .join() dot notation might be starting to feel a little funny after we've been exposed to "linking" functions together with compose, so let's use Ramda's join function to clean this up:

const responseToStatus =
  compose(join(' @ '), props(['message', 'timestamp']))

responseToStatus(iss) // "success @ 1617930803"

View this responseToStatus example in the Ramda REPL.

While this use case is admittedly small, there are times where we'll want to select only a few values from an object and have them in array format, and props is the right tool for helping us do that.

pick

Here's a new scenario for us to work with: our colleague needs an array of all the astronauts' names, but each array item must be an object with a key of name only.

Think we're up for the task? You bet!

astros.people.map(({ name }) => ({ name }))
// [
//  { name: "Sergey Ryzhikov" },
//  { name: "Kate Rubins" },
//  { name": "Sergey Kud-Sverchkov" },
//  { name: "Mike Hopkins" },
//  { name: "Victor Glover" },
//  { name: "Shannon Walker" },
//  { name: "Soichi Noguchi" }
// ]

This seems like a generic enough task that Ramda must surely have a helper function!

For the times we want to select a subset of keys and values from an object, we can use pick.

Similar to props, pick takes an array of keys and selects each key and its value from an object. Instead of returning an array of values, however, pick returns an object containing the keys and values you asked for.

This is great, for example, for whittling down an object in a data pipeline to only the properties that you need. Sending too many properties to a function can sometimes lead to confusion and even bugs!

Let's take our original implementation and use pick:

astros.people.map(pick(['name']))

View this initial pick example in the Ramda REPL.

Nice! But we're also doing the work of accessing the people property and calling map – both generic operations – so let's see if we can use Ramda's propOr and map helpers to compose something together.

const getAstroNames =
  compose(map(pick(['name'])), propOr([], 'people'))

getAstroNames(astros)

View this getAstroNames composition in the Ramda REPL.

While we're here, let's extract the reusable functions outo of this for potential future use:

const pickName      = pick(['name'])
const pickNames     = map(pickName)
const getPeople     = propOr([], 'people')
const getAstroNames = compose(pickNames, getPeople)

getAstroNames(astros)

View this refactored getAstroNames group of functions in the Ramda REPL.

pluck

Someone from marketing is trying to update our organization's emoji game, and they want us to display the astronauts' names separated by rocket ships (🚀). Here's what they want:

"Sergey Ryzhikov 🚀 Kate Rubins 🚀 Sergey Kud-Sverchkov 🚀 Mike Hopkins 🚀 Victor Glover 🚀 Shannon Walker 🚀 Soichi Noguchi"

Without thinking too hard about it, we come up with a simple solution:

astros.people.map(x => x.name).join(' 🚀 ')
// "Sergey Ryzhikov 🚀 Kate Rubins 🚀 ..."

We've been introduced to prop, so let's update that map:

astros.people.map(prop('name')).join(' 🚀 ')

As we hop on this refactor train, we wonder:

"What if we want to map a property other than name, and what if we want to call this map(prop('whatever')) on any list of objects?"

Thinking in generic terms, we establish that our list and our object key could be variable, so we make them variables in our function we'll call pluckKey:

const pluckKey = (key, xs) => xs.map(x => x[key])

// or with some nice ramda functions

const pluckKey = (key, xs) => map(prop(key), xs)

// and with some manual function currying
// (we'll cover this in depth in another chapter)

const pluckKey = key => xs => map(prop(key), xs)

// example
pluckKey('name')(astros.people)

With that last pluckKey function definition, we can link together a few functions to get the same result:

const pluckKey = key => xs => map(prop(key), xs)

const astrosPeopleWithRockets =
  compose(join(' 🚀 '), pluckKey('name'), propOr([], 'people'))

astrosPeopleWithRockets(astros)

Like before, we can pull those composed functions into named variables:

const joinRocket = join(' 🚀 ')
const pluckName  = pluckKey('name')
const getPeople  = propOr([], 'people')

const astrosPeopleWithRockets =
  compose(joinRocket, pluckName, getPeople)

This is looking pretty clean!

Naturally, this pluckKey function we so cleverly made has already been included in Ramda, and it's called pluck. It is equivalent to map(prop(key), list), just like our function definition, so we can delete our function and replace pluckKey('name') with Ramda's pluck. Here it is in its entirety:

const joinRocket = join(' 🚀 ')
const pluckName  = pluck('name')
const getPeople  = propOr([], 'people')

const astrosPeopleWithRockets =
  compose(joinRocket, pluckName, getPeople)

astrosPeopleWithRockets(astros)
// "Sergey Ryzhikov 🚀 Kate Rubins 🚀 ..."

View the pluck and astrosPeopleWithRockets set of functions in the Ramda REPL.

Testing Shallow Object Properties

Our destinations are Booleans – we reach them or we don’t – but our journeys are spectrums, because there are so many paths we can take to our destination that make getting there that much better.

A.J. Darkholme

In this chapter, we'll make logical decisions in our code (if/else) by testing our datasets' properties with a few boolean-returning helper functions.

Outline:

In case you missed it, we'll be using some ISS and astronaut data as our datasets.

ISS' Current Location
{
  "message": "success",
  "timestamp": 1617930803,
  "iss_position": {
    "latitude": "27.7270",
    "longitude": "133.2581"
  }
}
"How Many People Are In Space Right Now?"
{
  "message": "success",
  "number": 7,
  "people": [
    {
      "craft": "ISS",
      "name": "Sergey Ryzhikov"
    },
    {
      "craft": "ISS",
      "name": "Kate Rubins"
    },
    {
      "craft": "ISS",
      "name": "Sergey Kud-Sverchkov"
    },
    {
      "craft": "ISS",
      "name": "Mike Hopkins"
    },
    {
      "craft": "ISS",
      "name": "Victor Glover"
    },
    {
      "craft": "ISS",
      "name": "Shannon Walker"
    },
    {
      "craft": "ISS",
      "name": "Soichi Noguchi"
    }
  ]
}

We will assume we're storing those objects as variables named iss and astros.

has

We're working one morning, eating scones and refactoring some code, when a fellow developer, who lives in Iceland and started work a few hours before we did today, pings us with the following message:

Hjálp!

We shipped some astronaut code this morning that puts a method, toString, on some astros objects, and it joins all the astronauts' names together with rocket ships like this:

"Sergey Ryzhikov 🚀 Kate Rubins 🚀 ..."

Pretty cool, right?

We only want to use this method on the astros objects where it's defined, but we forgot that toString is already a defined method defined on an Object instance, so some astros objects are calling that method and returning "[object Object]" when we want them to do something else!

Can you help us? Takk!

Our Icelandic coworker then sends us the code:

const astrosWithToString = {
  "message": "success",
  "number": 7,
  "people": [/* omitted for brevity */],
  toString() {
    return astrosPeopleWithRockets(this)
  }
}

const astrosWithoutToString = {
  "message": "success",
  "number": 7,
  "people": [/* omitted for brevity */],
}

const astrosToString = data => {
  if ('toString' in data) { // THIS IS WHERE THE BUG HAPPENS!
    return data.toString()
  }

  return `There are ${data.number} astronauts`
}

// astrosToString(astrosWithToString) // this works
astrosToString(astrosWithoutToString) // this doesn't!

View this buggy astrosToString code in the Ramda REPL.

The astrosPeopleWithRockets code comes from the prior lesson on "Reading Shallow Object Properties", so check that out to see how we arrived at the nifty little helper functions you'll find in the REPL linked above.

Aha! We see where the misunderstanding happened. The 'toString' in data code is checking that there is a property defined on the object called toString — whether or not it inherited that property! All object instances have a .toString() method that they inherit, so it'll always have toString defined no matter what. What we want is to check if toString was explicitly defined by us on the astros object.

We first confirm our assumption and fix the bug by making this change:

// before
if ('toString' in data) {/*...*/}

// after
if (Object.prototype.hasOwnProperty.call(data, 'toString')) {/*...*/}

The hasOwnProperty exists on all objects, but it could be overwritten like toString was, so we use an external hasOwnProperty to do our check.

This check seems rather lengthy, so let's convert that to a function named hasProp:

const hasProp = (prop, obj) =>
  Object.prototype.hasOwnProperty.call(obj, prop)

Surprise! Ramda already has a helper for this, has, so we can replace our hasProp with has.

We then write a message back to our Icelandic colleague:

Hæ!

We found the issue, and it's a matter of testing whether we defined the object property or not. Here you go!

Eigðu góðan dag!

import { has } from 'ramda'

// ...

const astrosToString = data => {
  if (has('toString', data)) {
    return data.toString()
  }

  return `There are ${data.number} astronauts`
}

View the updated astrosToString and hasProp functions in the Ramda REPL.

propEq

One fine afternoon, our error monitoring service lets us know that our code is throwing errors when trying to access the latitude property from the ISS' location API response.

We quickly realize that if there is a problem with the API, we won't get the data back, so iss.iss_position.latitude won't work, for iss_position is undefined in the response:

{
  "message": "error",
  "timestamp": 1617930803
}

There are many ways to handle this error safely, but we're going to address it by simply checking if the message property is "success" or not:

if (iss.message === 'success') {
  // carry on...
}

Great! Call it a day!

...But our solution nags at us. We are accessing a property's value and equating it with an expected value. What if we made this a function?

const isMessageSuccess = data =>
  (data || {}).message === 'success' // or `data?.message === 'success'`

Note: the Ramda REPL doesn't currently support optional chaining like data?.message === 'success'.

Not bad, but we've simply moved the operations to a single place. What if we wrote a function that looked up any property on an object and then compared it with another value?

const doesPropEq = (key, val, data) =>
  (data || {})[key] === val // or `data?.[key] === val`

Nice! Let's try it out:

const isMessageSuccess = data =>
  doesPropEq('message', 'success', data)

isMessageSuccess(iss) // true

View doesPropEq and isMessageSuccess in the ramda REPL.

Now that we understand our need for doesPropEq, we can swap that out with ramda's propEq.

import { propEq } from 'ramda'

// ...

const isMessageSuccess = data =>
  propEq('message', 'success', data)

isMessageSuccess(iss) // true

Stopping the isMessageSuccess implementation work at this point is totally acceptable, but we can take it a little further.

Since all functions in ramda are auto-curried, that means that we can refactor isMessageSuccess like this:

// Step 0
const isMessageSuccess = data =>
  propEq('message', 'success', data)

// Step 1
const isMessageSuccess = data =>
  propEq('message', 'success')(data)

// Step 2
const isMessageSuccess =
  propEq('message', 'success')

// Step 3
// isMessageSuccess :: ISSData -> Bool
const isMessageSuccess =
  propEq('message', 'success')
  1. In Step 1, we demonstrate that propEq will accept our last argument as a separate function call (the result of calling propEq the first time will wait until it has all the arguments).

  2. In Step 2, we realize that accepting an argument and passing it on again is redundant, and so we can remove the need for a function closure and instead let the result of calling propEq with the first two values be what is bound to isMessageSuccess.

  3. In Step 3. we acknowledge that implicitly forwarding a function argument comes at the cost of remembering, "What am I passing in, again?" If you don't have a type-checker , you can provide some pseudo-types (these ones are in a Haskell style) where the data is defined in order to explain what ISSData is:

    // ISSData = { message      :: Message
    //           , timestamp    :: UnixTimestamp
    //           , iss_position :: LatLong
    //           }
    //
    // Message = 'success' | 'error'
    //
    // UnixTimeStamp = Number
    //
    // LatLong = { latitude  :: String
    //           , longitude :: String
    //           }
    

    The whole point of Step 3 is simply to identify what the expected input and output types are so that someone else (or you in 3 months) can easily understand a terse function at a glance.

Check out this propEq usage plus these pseudo-types in the ramda REPL.

propIs

TODO

propSatisfies

TODO

Introducing FP at Work

If your team isn't on the functional programming train already, it can be difficult to start using tools that introduce different paradigms like function composition, algebraic data types, and the heaps of function programming jargon.

One way of introducting functional programming concepts at your organization is by starting small: introduce a few functions and concepts at a time and keep things as familiar as possible.

For example, instead of writing code like

user.name || 'N/A'

you could include propOr like

import { propOr } from 'ramda'

propOr('N/A', 'name', user)

And then when you're mapping over a list of users to retrieve their names, you might think you could do the same thing,

users.map(x => x.name)

// changes to...

users.map(propOr('N/A', 'user'))

but then you realize that you might have a reusable function on your hands!

import { propOr } from 'ramda'

const nameOrDefault = propOr('N/A', 'name')

nameOrDefault(user)       // 'Fred'
users.map(nameOrDefault) // ['Fred', 'Wilma', 'N/A']

Taking this even further, what if we have two lists of users, users and otherUsers? Let's pull in map, as well!

import { map } from 'ramda'

map(nameOrDefault, users)      // ['Fred', 'Wilma', 'N/A']
map(nameOrDefault, otherUsers) // ['Wonder Woman', 'N/A', 'Batman']

// which can then be refactored to

const mapNameOrDefault = map(nameOrDefault)
mapNameOrDefault(users)      // ['Fred', 'Wilma', 'N/A']
mapNameOrDefault(otherUsers) // ['Wonder Woman', 'N/A', 'Batman']

Before you know it, you're finding quick and convincingly effective ways to bring Ramda (or whatever tool) into your day-to-day work.