组合函数如何处理多个参数?

4

这是一个我需要改进的“compose”函数:

const compose = (fns) => (...args) => fns.reduceRight((args, fn) => [fn(...args)], args)[0];

这是一个实际的应用实现示例:

const compose = (fns) => (...args) => fns.reduceRight((args, fn) => [fn(...args)], args)[0];

const fn = compose([
  (x) => x - 8,
  (x) => x ** 2,
  (x, y) => (y > 0 ? x + 3 : x - 3),
]);

console.log(fn("3", 1)); // 1081
console.log(fn("3", -1)); // -8

这里是我的导师提出的改进意见。

const compose = (fns) => (arg, ...restArgs) => fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);

如果我们像这样传递参数列表func(x,[y]) ,在第一次迭代时,我仍然不明白如何使用展开的数组[y]使函数工作?
1个回答

2
让我们分析改进后的compose函数的作用。
compose = (fns) =>
          (arg, ...restArgs) =>
          fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);

当您使用多个函数输入compose时,您将得到一个函数。在您的情况下,您给它命名为fn
那么这个fn函数是什么样子的呢?通过简单的替换,您可以将其视为以下内容:
(arg, ...restArgs) => fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);

其中 fns === [(x) => x - 8, (x) => x ** 2, (x, y) => (y > 0 ? x + 3 : x - 3)]

因此,您可以向这个函数 fn 提供一些参数,这些参数将与 (arg, ...restArgs) 进行“模式匹配”;在您的示例中,当您调用 fn("3", 1) 时,arg"3",而 restArgs[1](所以在逗号后面展开为 1),因此您可以看到 fn("3", 1) 简化为

fns.reduceRight((acc, func) => func(acc, 1), "3");

由此可见:

  1. 最右侧的函数 (x, y) => (y > 0 ? x + 3 : x - 3) 将以两个参数 "3"acc 的初始值)和 1 作为调用参数,
  2. 结果将作为第一个参数传递给下一次对 func 的调用,
  3. 以此类推,

但重点在于:func 的第二个参数 1 只会被最右侧的函数使用,它会被传递给其他两个函数,但是却被忽略了!

结论

函数组合是一种针对单参数函数的方法。¹ 如果与多参数函数一起使用,可能会导致混淆。²

例如,考虑以下两个函数:

square = (x) => x**2;   // unary
plus = (x,y) => x + y;  // binary

你能将它们组合起来吗?好的,你可以将它们组合成一个像这样的函数

sum_and_square = (x,y) => square(plus(x,y));

你问题底部的compose函数很适合:

sum_and_square = compose([square, plus]);

但是如果你的两个函数是这样的呢?

apply_twice = (f) => ((x) => f(f(x))); // still unary, technically
plus = (x,y) => x + y;                 // still binary

您的compose无法正常工作。

即使函数plus被柯里化了,例如如果它被定义为

plus = (x) => (y) => x + y

然后可以考虑将它们组合在一个函数中,该函数的作用如下:
f = (x,y) => apply_twice(plus(x))(y)

这将可预测地产生f(3,4) === 10

您可以使用f = compose([apply_twice, plus])来获得它。

美观上的改进

此外,我建议进行“美容”改变:使compose接受...fns而不是fns

compose = (...fns)/* I've only added the three dots on this line */ =>
          (arg, ...restArgs) =>
          fns.reduceRight((acc, func) => func(acc, ...restArgs), arg);

你可以直接调用函数进行组合,而不需要将要组合的函数分组成数组,例如你可以写compose(apply_twice, plus),而不是写compose([apply_twice, plus])
另外,lodash库中有两个函数可以处理函数组合:
(¹)这是Haskell的选择(在Haskell中,. 是组合运算符)。如果您将f . g . h应用于多个参数,则第一个参数将通过整个管道传递;该中间结果将应用于第二个参数;进一步的中间结果将应用于第三个参数,依此类推。换句话说,如果您在JavaScript中拥有了Haskell的组合函数haskellCompose,并且如果f是二元的,gh是一元的,则haskellCompose(f, g, h)(x, y)将等于f(g(h(x)), y)
(²)Clojure的comp则采取另一种选择。它将右侧的函数饱和,然后将结果传递给其他函数。因此,如果您在JavaScript中拥有了Clojure的组合函数clojureCompose,并且fg是一元的,而h是二元的,则clojureCompose(f, g, h)(x, y)将等于f(g(h(x,y)))
可能是因为我习惯于Haskell的自动柯里化函数,但我更喜欢Haskell的选择。

谢谢你的回答。有一件事我不太明白,就是“...restArgs在逗号后面展开为1”是什么意思?第二个值是如何从数组中解包出来的? - pokercatt
我不是专家,但是重点很简单:如果一个函数有一个名为 ...restArgs 的 _参数_,那么相应的 参数 restArgs 将是所有这些参数的数组;当在函数体中写入 ...restArgs 时,您会将该数组扩展为逗号分隔的内容。要查看区别,请在提示符下输入 ((...x) => [...x, x])(1,2,3) 并检查结果。 - Enlico

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接