surajs02
surajs02

Reputation: 471

Why does this composition function apply functions in the wrong order when passed as a reference?

Problem

The problem is that a composition function sometimes applies its functions in the wrong order but only when it is passed as a reference (e.g., mapValues(compose(appendB, appendA))(wordNumberMap)).

Whereas if the composition function is applied directly to a given value, it applies its functions in the correct order (e.g., mapValues(v => compose(appendB, appendA)(v))(wordNumberMap)).

composition-direct-vs-ref

Questions

  1. What causes passing the composition function by reference to apply its functions incorrectly?
  2. What (if possible) can be done to maintain the correct order of functions when passing the composition function by reference (e.g., mapValues(compose(appendB, appendA))?

Code

The following code shows all functions working individually and demonstrates how using the composition function directly works whilst passing the composition function by reference fails:

// All functions:
const appendA = v => v + 'a';
const appendB = v => v + 'b';
const appendC = v => v + 'c';
const compose = (...functions) => initial => functions.reverse().reduce((result, next) => next(result), initial);
const mapValues = (mapper = v => v) => (obj = {}) => Object.keys(obj).reduce((result, key) => ({ ...result, [key]: mapper(obj[key], key) }), {});

// `compose` works:
compose(appendC, appendB, appendA)(1); //=> "1abc"

// `mapValues` works:
const wordNumberMap = { one: 1, two: 2, three: 3, four: 4, five: 5 };
// `appendA` applied directly:
mapValues(v => appendA(v))(wordNumberMap); //=> {one: "1a", two: "2a", three: "3a", four: "4a", five: "5a"}
// `appendA` applied by reference:
mapValues(appendA)(wordNumberMap); //=> {one: "1a", two: "2a", three: "3a", four: "4a", five: "5a"}

// Applying `compose` directly applies the functions in the correct order:
mapValues(v => compose(appendC, appendB, appendA)(v))(wordNumberMap) //=> {one: "1abc", two: "2abc", three: "3abc", four: "4abc", five: "5abc"}

// Passing `compose` by reference applies the functions in the incorrect order:
mapValues(compose(appendC, appendB, appendA))(wordNumberMap); //=> {one: "1abc", two: "2cba", three: "3abc", four: "4cba", five: "5abc"}
// The returned object shows that every other entry in the returned object has a value where the append functions have been applied incorrectly in reverse order - why does this happen?

Upvotes: 0

Views: 109

Answers (1)

geoffrey
geoffrey

Reputation: 2474

In the following code you create a closure, which means your ...functions array is shared between calls

const compose = (...functions) => initial => ...

The root of the problem is that the reverse() method reverses an array in place, so if you call the return value of compose(appendC, appendB, appendA) multiple times (which is what you do when you "pass compose by reference"), you will reverse the same array time and time again.


This effect is mitigated when you "Apply compose directly" because you create a brand new composed function at each iteration:

mapValues(v => compose(appendC, appendB, appendA)(v))

so the fact that you mutate the ...functions inside compose is not a problem because the return value is short lived.


As a rule: never mutate your inputs in functional programming. It's a side-effect. you can mutate local variables all you like if it fits your style, but not your inputs.

Especially when you curry (the closure pattern a => b => ...) because you share the first input across calls.


You can implement compose like so:

const composition = (f, g) => x => g(f(x));
const id = x => x;
const compose = (...fns) => fns.reduceRight(composition, id);

Or if you prefer

const apply = (x, f) => f(x);
const compose = (...fns) => x => fns.reduceRight(apply, x);

Or if you need to apply multiple arguments to the entry point

const apply = (x, f) => f(x);
const compose = (...fns) => (...xs) => {
  const last = fns[fns.length -1];
  const init = fns.slice(0, -1)
  return init.reduceRight(apply, last(...xs))
};

Note that in the first version the iteration takes place when you call compose, whereas in the other two it occurs when you apply the returned function with arguments.

Upvotes: 1

Related Questions