1

I'm following an article about Transducers in JavaScript, and in particular I have defined the following functions

const reducer = (acc, val) => acc.concat([val]);
const reduceWith = (reducer, seed, iterable) => {
  let accumulation = seed;

  for (const value of iterable) {
    accumulation = reducer(accumulation, value);
  }

  return accumulation;
}
const map =
  fn =>
    reducer =>
      (acc, val) => reducer(acc, fn(val));
const sumOf = (acc, val) => acc + val;
const power =
  (base, exponent) => Math.pow(base, exponent);
const squares = map(x => power(x, 2));
const one2ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
res1 = reduceWith(squares(sumOf), 0, one2ten);
const divtwo = map(x => x / 2);

Now I want to define a composition operator

const more = (f, g) => (...args) => f(g(...args));

and I see that it is working in the following cases

res2 = reduceWith(more(squares,divtwo)(sumOf), 0, one2ten);
res3 = reduceWith(more(divtwo,squares)(sumOf), 0, one2ten);

which are equivalent to

res2 = reduceWith(squares(divtwo(sumOf)), 0, one2ten);
res3 = reduceWith(divtwo(squares(sumOf)), 0, one2ten);

The whole script is online.

I don't understand why I can't concatenate also the last function (sumOf) with the composition operator (more). Ideally I'd like to write

res2 = reduceWith(more(squares,divtwo,sumOf), 0, one2ten);
res3 = reduceWith(more(divtwo,squares,sumOf), 0, one2ten);

but it doesn't work.

Edit

It is clear that my initial attempt was wrong, but even if I define the composition as

const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);

I still can't replace compose(divtwo,squares)(sumOf) with compose(divtwo,squares,sumOf)

  • [`...` is not an operator!](https://stackoverflow.com/questions/37151966/what-is-spreadelement-in-ecmascript-documentation-is-it-the-same-as-spread-oper/37152508#37152508) – Felix Kling May 17 '17 at 13:32
  • @FelixKling I'm trying to write something that can do the transformation `(a,b,c,d, etc...) => a(b(c(d(etc...))))` –  May 17 '17 at 13:36
  • I understand the problem you are trying to solve. `...` is still not an operator ;) – Felix Kling May 17 '17 at 13:39
  • Transducers are harder to comprehend than you think. Here is a simplified `map` transducer: `map = f => g => x => y => g(x) (f(y))`. When you apply `map` to function composition `comp = f => g => x => f(g(x))`, it seems as if `comp` is able to compose more than two functions, because `x` is just another function. `comp` along with the composed transducers build a transducer stack, which is then evaluated top-to-bottom (and hence the composition seems to run left-to-right). This ability of `comp` is called abstraction over arity and is advanced functional programming stuff. –  May 17 '17 at 15:21
  • Uh, your `more` function only has two function parameters `f` and `g` that will be nested, how would you expect it to work with more arguments magically? – Bergi May 17 '17 at 16:15
  • @Bergi that's understood now, but my question is why with `const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);` I can't write `compose(divtwo,squares,sumOf)` instead of `compose(divtwo,squares)(sumOf)` –  May 17 '17 at 17:02
  • @user1892538 The problem is that `compose` always needs to return a function - like the `(...args) => …` from your `more`. Your `reduce` doesn't do that. – Bergi May 17 '17 at 18:23
  • @Bergi look at my edited answer, pls: I think it does the trick, do you agree that my solution is valid? –  May 17 '17 at 19:20

3 Answers3

3

Finally I've found a way to implement the composition that seems to work fine

const more = (f, ...g) => {
  if (g.length === 0) return f;
  if (g.length === 1) return f(g[0]);
  return f(more(...g));
}

Better solution

Here it is another solution with a reducer and no recursion

const compose = (...fns) => (...x) => fns.reduceRight((v, fn) => fn(v), ...x);
const more = (...args) => compose(...args)();

usage:

res2 = reduceWith(more(squares,divtwo,sumOf), 0, one2ten);
res3 = reduceWith(more(divtwo,squares,sumOf), 0, one2ten);

full script online

  • `reduceRight` takes exactly two arguments only. There's no point in using rest/spread syntax here. Just do `x => fns.reduceRight((v, f) = f(v), x)` – Bergi May 17 '17 at 22:08
  • @Bergi but when I try online, it seems it's different and in the end I would get an error "reducer is not a function". I'm on mobile now so I could be wrong but I opened the online link and I should have amended it as you told me... also, before, when I posted the answer I tried a lot of different possibilities and this was the only one successful... Thanks anyway, will double check –  May 17 '17 at 22:36
  • Ooops, I guess I misspelled the arrow function in my comment (forgot the `>`), but my point remains. – Bergi May 17 '17 at 22:47
  • @Bergi I noticed the arrow but that is *not* the point. Here is the [link](https://es6console.com/j2tkw4s8/): I'm on laptop now and I confirm I see the error "reducer is not a function" ([image](https://snag.gy/4pAczQ.jpg) of the console, but you have the text in the link at the beginning of this comment) –  May 17 '17 at 22:53
  • Sure, you're still calling `more` which doesn't work instead of the `compose` function with `reduceRight`. Here's the fixed code: https://es6console.com/j2tl7eu5/ – Bergi May 17 '17 at 22:58
  • @Bergi I see `[385,null,null]` from your link while when I call mine (last word "online" in the answer above) I get `[385,192.5,96.25]` –  May 17 '17 at 23:04
  • When I try to run the one you linked, I get `Uncaught ReferenceError: res1 is not defined` - the result variables are lacking a declaration and it's strict mode code. – Bergi May 17 '17 at 23:07
  • @Bergi I copy the EcmaScript6 with ctrl+A (select all) and ctrl+C (copy) and then I paste it (ctrl+V) in the Console below... I don't know, I'm confused –  May 17 '17 at 23:10
  • One should be able to press the [Run] button in the top left :-) But in any case, your `map` function is already doing the composition. You have to call `reduceWith(squares(divtwo(sumOf)), 0, one2ten);` like [here](https://es6console.com/j2tltfsv/), there's no need to `compose` anything. What you *could* do is something like `reduceWith(map(compose(x => x/2, x=>x**2))(sumOf), 0, one2ten)` where you compose functions to a new function that is then used in the `map`. – Bergi May 17 '17 at 23:18
  • So press [Run] from this [link](https://es6console.com/j2tm3gsi/): it seems ok to me –  May 17 '17 at 23:23
  • Yeah, but that's because `more` is invoking `compose` in a really horrible way (and that requires rest/spread to make `redureRight` run without an initial accumulator - urgh). You'd better do something like [this](https://es6console.com/j2tmclwq/) – Bergi May 17 '17 at 23:30
  • @Bergi ok, why don't you post it as answer? :-) –  May 17 '17 at 23:32
0

Your more operates with only 2 functions. And the problem is here more(squares,divtwo)(sumOf) you execute a function, and here more(squares,divtwo, sumOf) you return a function which expects another call (fo example const f = more(squares,divtwo, sumOf); f(args)).

In order to have a variable number of composable functions you can define a different more for functions composition. Regular way of composing any number of functions is compose or pipe functions (the difference is arguments order: pipe takes functions left-to-right in execution order, compose - the opposite).

Regular way of defining pipe or compose:

const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);

const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);

You can change x to (...args) to match your more definition.

Now you can execute any number of functions one by one:

const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);

const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);

const inc = x => x + 1;
const triple = x => x * 3;
const log = x => { console.log(x); return x; } // log x, then return x for further processing

// left to right application
const pipe_ex = pipe(inc, log, triple, log)(10);

// right to left application
const compose_ex = compose(log, inc, log, triple)(10);
Egor Stambakio
  • 17,836
  • 5
  • 33
  • 35
  • 1
    `const more = (f, g) => (...args) => f(g(...args));` combines 2 functions and applies combinations to `args` array as an argument. here `more(squares,divtwo,sumOf)` you're trying to define 3rd function which is not used anywhere in your `more` definition. – Egor Stambakio May 17 '17 at 11:28
  • 1
    The problem is here `more(squares,divtwo)(sumOf)` you execute a function, and here `more(squares,divtwo, sumOf)` you return a function which expects another call (fo example `const f = more(squares,divtwo, sumOf); .... f(args)`). And `compose` works fine if you add more functions, but also execute it with args: `compose(squares,divtwo,squares,divtwo)(sumOf)` : https://jsfiddle.net/wostex/ww7rdgsr/ . In order to call `more` with one pair of parenthesis, you'd define it like `const more = (f, g, ...args) => f(g(...args));` but it doesn't make sense to do so. – Egor Stambakio May 17 '17 at 11:49
  • "_pipe takes functions left-to-right in execution order, compose - the opposite_" I think this convention is very annoying. Either compose/pipe from right-to-left or the other way around. But not both. You're aware that transducers build a transformation stack that runs left-to-right, because `x` in `f(g(x))` is another function? So actually your transducers would be evaluated from right-to-left within your `pipe`. –  May 17 '17 at 12:43
0

I still can't replace compose(divtwo,squares)(sumOf) with compose(divtwo,squares,sumOf)

Yes, they are not equivalent. And you shouldn't try anyway! Notice that divtwo and squares are transducers, while sumOf is a reducer. They have different types. Don't build a more function that mixes them up.

If you insist on using a dynamic number of transducers, put them in an array:

[divtwo, squares].reduceRight((t, r) => t(r), sumOf)
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Well, I accept your answer from a theoretical standpoint, but you're evading the technical question :-) I wanted to mix those different types just for the sake of understanding the language possibilities... Btw Thanks a lot for your help –  May 17 '17 at 23:50
  • As we saw in the discussion below, there are many ways to get this to work - one of them would be `[divtwo, squares, sum].reduceRight((t, r) => t(r) /* no start value */)` - which can be abstracted into helper functions at various boundaries, possibly even using rest/spread syntax. – Bergi May 17 '17 at 23:57
  • 1
    Well - just to have a working link - this is the [compose ES6 Console](https://es6console.com/j2to06li/) for an arbitrary number of functions (transformers), from the code suggested in the original [article](http://raganwald.com/2017/04/30/transducers.html): i.e. `const compositionOf = (acc, val) => (...args) => val(acc(...args));` and then `const compose = (...fns) => reduceWith(compositionOf, x => x, fns);` with usage/example `compose(divtwo,squares)(sumOf)`, so the reducer `sumOf` remains separated. If you want to share your favourite version of the script, welcome –  May 18 '17 at 00:38