Point-free composition of lenses in Ramda.js

Viewed 259

I am trying to compose functions that return lenses, to produce a new lens, and do it in a point-free style.

This is probably a more general question about function composition. Lenses are just a case-study. I am not interested in lenses specifically, but I want to know the general pattern for how to compose these functions in a point-free way.

const obj = {a: {x: 0}, b: {x: 42}};

// this won't work, but I want it to work
const pointFreeComposedLens = R.compose(R.lensProp, R.lensProp('x'));
R.view(pointFreeComposedLens('a'), obj); // returns 'undefined'

// this works
const pointyComposedLens = key => R.compose(R.lensProp(key), R.lensProp('x'));
R.view(pointyComposedLens('a'), obj); // returns '0'

What is the pattern for composing functions so that I don't need to keep re-writing the arguments for the first function in the composition pipeline?

For an egregious example:

const deepLens = (a, b, c) => R.lensPath([a, b, c]);

// This works, but is tedious & verbose
const extraDeep = (a, b, c, x) => R.compose(deepLens(a,b,c), R.lensProp(x));
const gammaDeep = (a, b, c, y) => R.compose(deepLens(a,b,c), R.lensProp(y));

// Doesn't work, but it would be nicer to write:
const extraDeep = x => R.compose(deepLens, R.lensProp(x));

// and call it like so:
R.view(extraDeep('a','b','c','x'), obj);
3 Answers

I know you're looking at lenses only as an example, but here is one way to get something like the behavior I think you want from them.

const {lensPath, compose, lens, view} = R

const deepLens = (a, b, c) => lensPath([a, b, c]);
const deeper = (lens, ...args) => compose(lens, lensPath(args))

const cLens = deepLens('a', 'b', 'c')
const obj =  {a: {b: { c: {d: 1, e: 2, f: {g: 3, h: 4, i: {j: 5, k: 6}}}}}}

console.log(view(cLens, obj)) //=> {d: 1, e: 2, f: {g: 3, h: 4, i: {j: 5, k: 6}}}
console.log(view(deeper(cLens, 'f', 'g'), obj)) //=> 3

const fLens = deeper(cLens, 'f')

console.log(view (fLens, obj)) //=> {g: 3, h: 4, i: {j: 5, k: 6}}

const jLens = deeper(cLens, 'f', 'i', 'j')
// or jLens = deeper(fLens, 'i', 'j')

console.log(view(jLens, obj)) //=> 5
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

As to the broader composition question, lenses are generally a special case for a library like Ramda, as the composition is in the opposite order than usually expected. (The technical reasons are too much to go into here.)

But that's why this doesn't work:

const extraDeep = x => R.compose(deepLens, R.lensProp(x));

Ramda does allow the first function in a composition chain (rightmost in compose, leftmost in pipe to receive additional arguments. But when the composition order is reversed with lens composition, it doesn't do what you might like.

So if you are having similar issues with composition in another context, please open a separate question. I'd be curious to see what you're looking for.

Rest parameters will shorten the code to:

 const extraDeep = (...rest) => last => R.compose(deepLens(...rest), R.lensProp(last))(rest.pop());

but I'm not sure if that is really elegant.

If your intention is to write a function that accepts a path and an object, then path already exists:

R.path(['a', 'b'], {a: {b: 10}}); //=> 10

If you're interested in removing some parameters in some of your functions, deepLens could be rewritten as follow:

const deepLens = R.unapply(R.lensPath);

This point-free version has the added benefit that it is not limited to just three parameters. It will work with any number of parameters:

deepLens('a', 'b');           //=> R.lensPath(['a', 'b']);
deepLens('a', 'b', 'c');      //=> R.lensPath(['a', 'b', 'c']);
deepLens('a', 'b', 'c', 'd'); //=> R.lensPath(['a', 'b', 'c', 'd']);
Related