Functional way of re-using variables in a pipe

2020-07-23 04:45发布

问题:

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.

回答1:

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.



回答2:

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



回答3:

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!



回答4:

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>