Using functional programming in javascript and typescript together with Ramda, I often find myself writing code like:
const myFun = c => {
const myId = c.id
const value = pipe(
getAnotherOtherPropOfC,
transformToArray,
mapToSomething,
filterSomething,
// ... N other transformations
// ok now I need myId and also the result of the previous function
chainMyIdWithResultOfPreviousFunction(myId)
)(c)
return value
}
Notice how creating a const myId
breaks point-free style. I'd like to write myFun
so that there's no need to explicit c
. So something like:
const myFun = pipe(....)
I was wondering if there's a more functional and readable way of doing things like this.
Can it be done? Sure. Should it be done? It's not so clear.
Here is a point-free version of the above, using lift
:
const getAnotherOtherPropOfC = prop ('x')
const transformToArray = split (/,\s*/)
const mapToSomething = map (Number)
const filterSomething = filter (n => n % 2 == 1)
const square = (n) => n * n
const chainMyIdWithResultOfPreviousFunction = (id) => (arr) => `${id}: [${arr}]`
const myFun = lift (chainMyIdWithResultOfPreviousFunction) (
prop ('id'),
pipe(
getAnotherOtherPropOfC,
transformToArray,
mapToSomething,
filterSomething,
map (square)
)
)
console .log (
myFun ({id: 'foo', x: '1, 2, 3, 4, 5'}) // => 'foo: [1,9,25]'
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script> const {prop, split, map, filter, lift, pipe} = R </script>
lift
is a more FP-standard function than Ramda's converge
(which together with useWith
offer ways to make point-free solutions, often at the expense of readability.) lift
overlaps with converge
when applied to functions, but is designed for unary functions, where converge
handles polyadic ones.
This is not horrible. But the only advantage it has over the original is that it's point-free. And if you were to try to extend this to intermediate functions in that pipeline, it would get downright ugly.
My take is that point-free can at times lead to cleaner, easier-to-read, and easier-to-maintain code. But there is no reason to go point-free when it doesn't do so.
It's not that point-free is inherently more functional than pointed code. I think this idea starts as a sort of Haskell-envy from other languages. Haskell is held up as an idealized FP language, and it happens to be a language in which point-free comes naturally. But that's at least partially coincidental.
My standard example is that const sum = reduce(add, 0)
is cleaner and more comprehensible than const sum = (xs) => xs.reduce(add, 0)
. It also makes extremely clear the parallels with const product = reduce(multiply, 1)
or const all = reduce(and, true)
. But as you get more complex, or when you need to reuse an intermediate calculation (as Bergi pointed out), point-free code often becomes a liability.
I don't have a real call here about whether this version is an improvement on the original. If so, it's only a minor one. Carrying it further would significantly degrade the readability.
Variables are usually an indication that your function is doing too much and should be decomposed. For example, this
const myFun = c => {
let id = c.id;
return R.pipe(
R.prop('list'),
R.map(R.add(10)),
R.sum,
R.subtract(id),
)(c)
}
can be refactored into two separate functions:
const compute = R.pipe(
R.prop('list'),
R.map(R.add(10)),
R.sum,
);
const getId = R.prop('id');
and then simply
const myFun = c => getId(c) - compute(c)
which looks good enough to me, but if you want to be absolutely point-free, then
const myFun = R.converge(R.subtract, [getId, compute])
Playground
Please note that I use vanilla JS to reach a wider audiance.
A pipe
is just a composite function is just a function. So let's use a higher order pipe that takes a pipe, a function and a value and returns another function instead of the result. To actually get the result we need to pass the value twice:
const toUc = s => s.toUpperCase();
const double = s => `${s}${s}`;
const shout = s => `${s}!`;
const concat = t => s => `${s} -> ${t}`;
const pipe = g => f => x => f(g(x));
const pipe3 = h => g => f => x => f(g(h(x)));
const foo = {id: "hi"};
const bar = pipe3(toUc) (shout) (double); // pipe
const baz = pipe(bar) (concat); // higher order pipe
// the result of the applied pipe is just another function because concat requires two arguments!
console.log(
baz(foo.id)) // s => `${s} -> ${t}`
// let's apply it twice
console.log(
baz(foo.id)
(foo.id)); // hi -> HI!HI!
What about something like this:
const getAnotherPropOfC = R.prop('message');
const transformToArray = R.split('');
const mapToSomething = R.map(R.toUpper);
const filterSomething = R.reject(R.equals(' '));
const chainMyIdWithResultOfPreviousFunction = R.pipe(
R.prop('fragment'),
R.concat,
);
const myFun = R.converge(R.map, [
chainMyIdWithResultOfPreviousFunction,
R.pipe(
getAnotherPropOfC,
transformToArray,
mapToSomething,
filterSomething,
),
]);
console.log(
myFun({ message: 'Hello World', fragment: '!' }),
);
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>