A Step Beyond Intermediate Functional Programming Patterns: Investigating Lenses with Ramda
February 22, 2024
Intro
In June 2023 I explored intermediate functional programming patterns in JavaScript.
That article gave the following kinds of functional programming:
Personally I’ve come to the - somewhat incremental but somewhat not - kinds of functional programming:
Declarative data transformations: someArray.map( (x) => x+ 1): much better than a for loop, especially as you add primitives like filter or select into the mix. Build on the provided blocks!
Traversal systems. Why write all that code to get to the deeply nested part of the data, you can write a descriptive path?
Declarative program logic: Pattern matching, expressions that return results (let res = if (…)), etc
Lenses: even better traversal systems!
Result/Option types: avoid that nasty if statement all together with someResult.getOrDefault(‘foo’).
Mutable state is bad, mmmkay?
Monads
??? type theory??? Functors and effects, etc etc
Passing around global, read only, state.
The article then dove straight into traversal systems and declarative program logic, leaving several topics unexplored.
Now is time to explore the next topic: Lenses: even better traversal systems with the JavaScript library Ramda.
About Ramda
Ramda provides many utility functions you’d find in a “utility toolkit” like lodash, but makes explicit “functional programming” style choices. For example, most functions in Ramda are automatically curried, allowing even more composable functional parts.
It provides a few larger areas of functionality, beyond just utilities. One of them is lens support.
Deep Selection and Traversal Systems made simple with Lenses
Early in my Rails career I was on a large, complex, Rails project. In modeling the problem domain we found ourselves with deep and complex object graphs.
We may have written code like:
allFavoriteTVShowsNames = current_user.favorites.tv.shows.map( { |i| i.name }
This favorites
method would return an object, with a tv
method on it, which would return instances of (let’s imagine) a TVShow
object. (In a less elegant language the method calls would be more explict: current_user.favorites().tv().shows()
, for example)
In our project we found ourselves thinking about the Law of Demeter, and solving it in typical OOP ways: mostly by creating (or metaprogramming) more methods on classes.
Because of this abstraction from the underlaying data model we could either make shortcut methods: current_user.tv_shows
, or refactor tv
to return TVShow
when it’s really stored in a new MovingPictureEntertainment
object.
Now, JavaScript projects tend to focus on passing around dictionaries packed with data, not objects that return other objects. In a JavaScript project you’re more likely to have the following
const current_user = {
favorites: {
tv: {
shows: [
{name: "Wandavision"},
{name: "The Mandalorian"}
]
}
}
}
Not an object to be found, no place to add accessor methods to make that traversal less fragile in face of refactorings, or easier to use.
But the problems Demeter points out are still there! We want to avoid any one method from becoming tightly coupled to another object’s structure. When we want to refactor this data structure in the future we may have multiple places to change.
We could manually create accessor methods and put them in a library:
// tvLib.js
export function getShows(obj) {
return tv.shows
}
// main.js
const showNames = getShows(current_user.favorites)
Which helps, but at the cost of boilerplate and inconvenience.
But can we create accessor methods like this easily, without breaking the flow of getting things done?
The functional concept of lenses gives us this ability: the ability to easily create functions that extracts data from structures.
Ramda’s lensProp method returns a lens for a given property:
const favorites = R.lensProp('favorites')
Used like so:
R.view(favorites, current_user)
.
But we have an object path. lensPath to the rescue
const showsLens = R.lensPath(['favorites', 'tv', 'shows'])
R.view(showsLens, current_user)
Or compose them:
const showsLens = R.lensPath(['favorites', 'tv', 'shows'])
const showName = R.lensProp('name')
R.view(showsLens, current_user).map( R.view(showName) )
If you find you’re using that lens often, easily move that single line into a library.
Ramda, automatically curries methods without enough parameters. So R.view(showName)
here technically returns us a partial function, which then map
calls with the current element in the iteration. Clever.
We’ve turned our complex object path into a simpler, abstract thing. If the internals of our current_user
object change we change the makeup of the lens not the business code. Functional, declarative accessors on data objects, unreliant on classes.
Select it like SQL
If we think about querying, transforming and performing other operations on data in a declarative way, there’s entire classes of languages devoted to this problem. The big one being SQL (Structured Query Language), but with various QL specs being created in the last decade or so (GraphQL, CodeQL).
Functional programming also should give us easy constructs to interact with, and process data. We have collect
, select
, reduce
etc, but they still don’t come close to the elegance of the following SQL command:
SELECT names.firstName as fName, person.ID as id WHERE person.ID = 'rpw' FROM people INNER JOIN names.id = people.name_id
<— yes this is untested, forgive syntax errors.
Here we have a table of users and a table of names, which we join together on matching fields. Then we select all records that have the ID of rpw
then select the first name out of it.
Can we easily do a similar thing with intermediate functional primitives? Kinda.
The joining part is relatively easy
const names = []
const people = []
const joined = people.map( (p) => {
const foundName = names.select( (n) => n.id == p.name_id )
if (foundName) {
return Object.assign(p, foundName)
} else {
return null
}
})
Ugly, and a lot of code compared to the declarative nature of SQL. Let’s refactor.
innerJoin function
If we refactor that we get an interesting function:
function innerJoin(collectionA, collectionB, joiner) {
const output = []
collectionA.forEach( (a) => {
const foundMatch = joiner(a, collectionB)
if (foundMatch) {
output.push( Object.assign(a, foundMatch))
}
})
return output
}
With this we can trivially construct a pipeline as such:
const res = innerJoin(
people,
names,
(element, namesArray) => namesArray.find( (p) => p.id == element.name_id )
).filter( (p) => p.firstName == 'Ryan' )
Next SQL problem: remapping property names (with, you guessed it, lenses)
But our SELECT names.firstName as fName
wants us to remap the field named firstName
into a property named fName
.
We can write a relatively simple function that applies the named result of applying lenses to the input.
Here we use reduce mostly out of laziness.
function remapProperties(specifications, collection) {
return collection.map( (current) => {
return R.reduce( (accumulator, name) => {
accumulator[name] = R.view( specifications[name], current)
return accumulator
}, {}, Object.keys(specifications))
})
}
And use it like so, in our final form:
remapProperties(
{id: R.lensProp('id'), fName: R.lensProp('firstName')},
innerJoin(
people,
names,
(element, namesArray) => namesArray.find( (p) => p.id == element.name_id )
).filter( (p) => p.firstName == 'Ryan' )
)
Oddly enough, it looks kind of strange to the eye, but it does map the operations found in the original SQL, with simpler, declarative, feel. All built up with both functional programming primitives and the data access abstractions lenses give us!
Returning to and remapping our planets structure
Returning to an example from the previous blog article, we have the following data structure: a deep object response from the Star Wars API
const planetResp = {
"data": {
"allFilms": {
"films": [
{
"planetConnection": {
"planets": [
{
"name": "Fobar",
"population": 200000
},
{
"name": "Bazbar",
"population": 2000000000
},
{
"name": "Jarvar IV",
"population": 1000
}
]
}
},
{
"planetConnection": {
"planets": [
{
"name": "Zero",
"population": 10
},
{
"name": "Nope",
"population": 42
}
]
}
}
]
}
}
}
From which we wanted to get the list of all the planets in any Star Wars movie.
Our code, in that previous article, used the node-json-transform library:
const output = []
const res = transform(json_object, {item: {planets: 'data.allFilms.films'},
operate: [
{
run: (x) => {
transform(x,
{
item: {pz: 'planetConnection.planets'},
operate: [
{
run: (y) => output.push(...y),
on: 'pz'
}
]
}
)
return output
},
on: 'planets'
}
]
}
)
Which looks transformational, but awkward. Less awkward then the code we’d normally write, but awkward.
We can reconstruct this with lenses and normal functional tools from ES6+:
R.view(R.lensPath(['data', 'allFilms', 'films']), planetResp)
.flatMap( (currentFilm) => R.view(R.lensPath(['planetConnection', 'planets']), currentFilm) )
Focus in on just the films, then for each film traverse planetConnection to the planets, but know that will return you multiple items so use flatMap
to concatinate multiple returned items onto the resulting list (vs map
which would result in lists of lists).
Conclusion
Ramda is neat, initially giving basic functional utility functions but letting advanced developers grow and solve complex problems elegantly! There’s more to lenses than just getters and more to Ramda than just functional utilities and lenses, so check it out!