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
- Steve Purr (review) (GitHub)
- Tom Wilson (review) (GitHub, hyper cloud service)
- Ian Greulich (review) (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
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)
const log = x => console.log(x)
[1, 2, 3].forEach(log)
// 1
// 2
// 3
Map, Filter, & 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
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
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:
- multiply the Celsius value by the result of
9 / 5
- 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...
- adding
32
to the result of - multiplying the Celsius value by the result of dividing
9
by5
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:
- Mathematics writes
f(x)
and not(x)f
- 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,
- get the
timestamp
- multiply the timestamp by
1000
to convert it to milliseconds - 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:
- from the ISS object, we get the shallow property,
timestamp
- we multiply that value by
1000
- 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:
- we need to have the
latitude
fall back to a previouslatitude
value if the current one is unattainable - the value needs to be a floating point
number
and not astring
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:
- a fallback value of some type
a
- a property name as a
String
- 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 someastros
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 thattoString
is already a defined method defined on anObject
instance, so someastros
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')
-
In Step 1, we demonstrate that
propEq
will accept our last argument as a separate function call (the result of callingpropEq
the first time will wait until it has all the arguments). -
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 toisMessageSuccess
. -
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.