Thinking in Ramda: Immutability and Objects
This post is Part 6 of a series about functional programming called Thinking in Ramda.
In Part 5, we talked about writing our functions in “pointfree” or “tacit” style where the main data argument to our function is not explicitly shown.
At that time, we were unable to convert all of our functions to pointfree style because we were missing some tools. It’s now time to learn those tools.
Reading Object Properties
Let’s look back at the eligible voters example that we revisited in Part 5:
As you can see, we’ve made
isEligibleToVote pointfree, but we couldn’t do that with the first three functions.
As we learned in Part 4, we can make the functions more declarative using
gte. Let’s start there.
In order to make these functions pointfree, we need a way to build up a function that we can then apply to the
person at the end. The problem is that we need to access properties on the
person and the only way we know how to do that is imperatively.
Fortunately, Ramda can help us out. It provides the
prop function for accessing properties of an object.
prop, we can turn
prop('birthCountry', person). Let’s start with that.
Wow, that looks a lot worse now. But let’s keep going with the refactoring. First, let’s swap the order of the arguments we’re passing to
equals so that
prop comes last.
equals works the same with either order, so this is safe.
Next, let’s use the curried nature of
gte to make new functions that we apply to the result of the
That’s still a bit worse, but let’s keep going anyway. Let’s take advantage of currying again with all of the
Worse again. But now we see a familiar pattern. All three of our functions have the same shape as
f(g(person)), and we know from Part 2 that this is equivalent to
Let’s take advantage of that.
Now we’re getting somewhere. Now all three functions look like
person => f(person). We know from Part 5 that we can make these functions pointfree.
It wasn’t obvious when we started that our methods were doing two different things. They were both accessing a property of an object and performing some operation on the value of that property. This refactoring to pointfree style has made that very explicit.
Let’s look at some more tools that Ramda provides for working with objects.
prop reads a single property from an object and returns the value,
pick reads multiple properties from an object and returns a new object with just those properties.
For example, if wanted just the names and ages of a person, we could use
pick(['name', 'age'], person).
If we just want to know if an object has a property without reading the value, we can use
has for checking own properties, and
hasIn for checking up the prototype chain:
prop reads a property from an object,
path dives into nested objects. For example, we could access the zip code from a deeper structure as
path(['address', 'zipCode'], person).
path is more forgiving than
path will return
undefined if anything along the path (including the original argument) is
prop will raise an error.
propOr / pathOr
pathOr are similar to
path combined with
defaultTo. They let you provide a default value to use if the property or path cannot be found in the target object.
For example, we can provide a placeholder when we don’t know a person’s name:
propOr('<Unnamed>', 'name', person). Note that unlike
propOr will not raise an error if
undefined; it will instead return the default value.
keys / values
keys returns an array containing the names of all of the own properties in an object.
values returns the values of those properties. These functions can be useful when combined with the collection iteration functions we learned about in Part 1.
Adding, Updating, and Removing Properties
We now have lots of tools for reading from objects declaratively, but what about when we want to make changes?
Since immutability is important, we don’t want to change objects directly. Instead, we want to return new objects that have been changed in the way we want.
Once again, Ramda provides a lot of help for us.
assoc / assocPath
When programming imperatively, we could set or change the name of a person with the assignment operator:
person.name = 'New name'.
In our functional, immutable world we use
const updatedPerson = assoc('name', 'New name', person).
assoc returns a new object with the added or updated property value, leaving the original object unchanged.
There is also
assocPath for updating a nested property:
const updatedPerson = assocPath(['address', 'zipcode'], '97504', person).
dissoc / dissocPath / omit
What about deleting properties? Imperatively, we might want to say
delete person.age. In Ramda, we’d use
const updatedPerson = dissoc('age', person).
dissocPath is similar, but works deeper into the object structure:
dissocPath(['address', 'zipCode'], person).
There is also
omit, which can remove several properties at once.
const updatedPerson = omit(['age', 'birthCountry'], person).
omit are quite similar and complement each other nicely. They’re very handy for white-listing (keep only this set of properties using
pick) or black-listing (get rid of this set of properties using
We now know enough to work with objects in a declarative and immutable fashion. Let’s write a function,
celebrateBirthday, that updates a person’s age on their birthday.
This is a pretty common pattern. Rather than updating a property to a known new value, we really want to transform the value by applying a function to the old value as we’ve done here.
I don’t know of a good way to write this with less duplication and in pointfree style given the tools we know about.
Ramda to the rescue once more with its
evolve function. I introduced
evolve in a previous post.
evolve takes an object that specifies a transformation function for each property to be transformed. Let’s refactor
celebrateBirthday to use
This code is saying to evolve the target object (not shown here because of pointfree style) by making a new object with the same properties and values, but whose
age is obtained by applying
inc to the original value of
evolve can transform multiple properties at once and at multiple levels of nesting. The transformation object can have the same shape as the object being evolved and
evolve will recursively traverse both structures, applying transformation functions as it goes.
evolve will not add new properties; if you specify a transformation for a property that doesn’t appear in the target object,
evolve will just ignore it.
I’ve found that
evolve has quickly become a workhorse in my applications.
Sometimes, you’ll want to merge two objects together. A common case is when you have a function that takes named options and you want to combine those options with a set of default options. Ramda provides
merge for this purpose.
merge returns a new object containing all of the properties and values from both objects. If both objects have the same property, the value from the second argument is used.
Having the second argument win makes sense when using
merge by itself, but less so in a pipeline situation. In that case, you’re often performing a series of transformations to an object, and one of those transformations is to merge in some new property values. In this case, you want the first argument to win.
merge(newValues) in the pipeline will not do what you expect.
For this situation, I typically define a utility function called
reverseMerge. It can be written as
const reverseMerge = flip(merge). Recall that
flip reverses the first two arguments of the function it is applied to.
merge performs a shallow merge. If the objects being merged both have a property whose value is a sub-object, those sub-objects will not be merged. Ramda does not currently have a “deep merge” capability, where sub-objects are merged recursively.
merge only takes two arguments. If you want to merge multiple objects into one, there is
mergeAll that takes an array of the objects to be merged.
This has given us a nice set of tools for working with objects in a declarative and immutable way. We can now read, add, update, delete, and transform properties in objects without changing the original objects. And we can do these things in a way that works when combining functions.
Now that we can work with objects in an immutable way, what about arrays? Immutability and Arrays shows us how.