Composing lenses in Ramda
Composing lenses in Ramda

Composing lenses in Ramda

Written on

The lens is a first-class citizen (Function) that gives us access (focus) to the particular piece of the complex data structure. We'll look into the basic rules of lens composition.

If you are a fan of point-free functional programming in Javascript, you will inevitably become a fan of Ramda. Ramda is a functional library with an emphasis on immutability and side-effect-free functions.

Ramda contains the implementation of one of my favorite functional concepts - functional lenses. Lenses are part of category theory and allow us to focus on a particular piece/path of a complex data object.

1
2
3
4
5
6
import { lensPath, view } from 'ramda';

const complexObject = { level1: { level2: { prop1: 1, prop2: 2 } } };
const prop1Lens = lensPath(['level1', 'level2', 'prop1']);

console.assert(view(prop1Lens, complexObject) === 1);

As you can see, lenses are a pretty powerful concept. But as you went through the code, it seems that you are constrained to creating particular lenses for particular use-cases. But that is a wrong assumption, as I found out myself. What I am about to show you is not documented even in Ramda documentation or its recipes.

Lenses are basically just ordinary functions, and as a regular function, they can be composed. Lenses themselves are compo-sable. I call them combined lenses. Here is the code snipped that is self-explanatory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { lensPath, compose, view } from 'ramda';

// Generic lenses
// --------------

const enabledLens = lensPath(['enabled']);

// Combined lenses
// ---------------

const sshServiceLens = lensPath(['sshService']);
const sshServiceEnabledLens = compose(sshServiceLens, enabledLens);

const telnetServiceLens = lensPath(['telentService']);
const telnetServiceEnabledLens = compose(telnetServiceLens, enabledLens);

// Usage
// -----

const services = {
  sshService: { enabled: true },
  telnetService: { enabled: false },
};

console.assert(view(sshServiceEnabledLens, services) === true);
console.assert(view(telnetServiceEnabledLens, services) === false);

The key here is to always use lensPath instead of lensProp. If you use lensProp, you may end up in TypeError. Use compose function from Ramda and compose the lenses from left to right (why?).

Lens composition from generic lenses to combined lenses is one step further in using the functional lenses. So don’t waste your time and try it in your next project.

Functional Lenses in JavaScript series:

Fork me on GitHub