为什么这个组合函数在作为引用传递时以错误的顺序应用函数?

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

问题

问题是组合函数有时会以错误的顺序应用它的函数,但只有当它作为引用传递时(例如,mapValues(compose(appendB, appendA))(wordNumberMap))。

而如果组合函数直接应用于给定值,它会以正确的顺序应用其函数(例如,mapValues(v => compose(appendB, appendA)(v))(wordNumberMap))。

问题

  1. 是什么原因导致通过引用传递组合函数而错误地应用其函数?
  2. 在通过引用传递组合函数时(例如,mapValues(compose(appendB, appendA))?
  3. ,可以做些什么(如果可能)来保持函数的正确顺序

代码

以下代码显示了所有单独工作的函数,并演示了如何在通过引用传递组合函数失败时直接使用组合函数工作:

// 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?

在下面的代码中,您创建了一个闭包,这意味着您的 ...functions 数组在调用之间共享

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

问题的根源在于the reverse() method reverses an array in place,所以如果你多次调用compose(appendC, appendB, appendA)的return值(这就是你在“通过引用组合),你将一次又一次地反转同一个数组。


当您“直接应用组合”时会减轻这种影响,因为您在每次迭代时都会创建一个全新的组合函数:

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

所以您在 compose 中改变 ...functions 不是问题,因为 return 值是短暂的。


作为一条规则:永远不要改变函数式编程中的输入。这是一个副作用。如果适合您的风格,您可以随心所欲地改变局部变量,但不适合您的输入。

特别是当您柯里化(闭包模式 a => b => ...)时,因为您在调用之间共享第一个输入。


您可以像这样实施 compose

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

或者如果您愿意

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

或者如果您需要将多个参数应用于入口点

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))
};

请注意,在第一个版本中,迭代在您调用 compose 时发生,而在其他两个版本中,它在您应用带参数的 returned 函数时发生。