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: 2021-04-27

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

  • Steve Purr (review, ideas) (GitHub)

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

Following the instructions from Ramda's homepage, to install Ramda into a project via NPM, you run

λ npm i ramda

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

Outline:

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

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

props

pick

pick

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.