JavaScript 和 ES6 中柯里化函数的函数应用

Function application for curried functions in JavaScript and ES6

我喜欢 ECMAScript 6 允许您像这样编写柯里化函数:

var add = x => y => z => x + y + z;

但是,我讨厌我们需要用括号括起柯里化函数的每个参数:

add(2)(3)(5);

我希望能够同时将柯里化函数应用于多个参数:

add(2, 3, 5);

我该怎么办?我不关心性能。

大多数人这样写柯里化函数:

var add = curry(function (x, y, z) {
    return x + y + z;
});

add(2, 3, 5);

主要是因为他们不想写这个:

var add = function (x) {
    return function (y) {
        return function (z) {
            return x + y + z;
        };
    };
};

add(2)(3)(5);

然而,nobody agrees on how to implement .

然后,ECMAScript 6 为我们解决了第一个问题:

var add = x => y => z => x + y + z;

但是,我们还是要自己解决第二个问题:

add(2)(3)(5);

是时候解决这个问题了:

var $ = (func, ...args) => args.reduce((f, x) => f(x), func);

希望你喜欢 Lisp 语法:

$(add, 2, 3, 5);

对不起jQuery。函数应用更基础


此外,Bergi 的 很棒:

const uncurry = func => (...args) => {
    var result = func;
    for (let arg of args)
        result = result(arg);
    return result;
}

var add = uncurry(x => y => z => x + y + z);

add(2, 3, 5);

不过,我还是比较喜欢用$

您可以轻松地编写一个函数,将多个参数应用于这样一个柯里化函数:

const uncurry = fn => (...args) => args.reduce((f, x) => f(x), fn);

// or alternatively:
const uncurry = fn => (...args) => {
    let f = fn;
    for (const x of args) f = f(x);
    return f;
}

现在您可以像这样调用 add

uncurry(add)(2, 3, 4)

如果您仍然讨厌它,您也可以使用

const $ = uncurry(uncurry);

$(add, 2, 3, 4)

柯里化和柯里化函数的应用在 Javascript 中是有争议的问题。简单来说,有两种相反的观点,我简单地说明一下。


- 仅在必要时使用单独的 curry 函数

从其他语言或范例中改编概念原则上是一件好事。但是,这种改编应该使用目标语言的基本方法来完成。这对 javascript 中的柯里化意味着什么?

  • 柯里化函数被称为一元函数序列:add3(1)(2)(3); // 6
  • 自己的函数用箭头手动柯里化const add3 = x => y => z => x + y + z;
  • 第三方函数或方法由单独的咖喱函数咖喱

- 默认使用单独的 curry 实现

建议的 $/uncurry 函数有问题:

const $ = (func, ...args) => args.reduce((f, x) => f(x), func);
const sum = x => y => z => x + y + z;

$(sum, 1, 2, 3); // 6
$(sum, 1, 2)(3); // 6
$(sum, 1)(2, 3); // z => x + y + z

这样,非柯里化函数只能应用一次,参数数量不受限制。任何后续调用都必须是一元的。该函数完全按照它的承诺行事。但是,它不允许应用 curried 函数,例如 JavaScript 开发人员习惯的。大多数当前的 curry 实现都更加灵活。这是一个扩展的实现:

const uncurry = f => (...args) => args.reduce(
  (g, x) => (g = g(x), typeof g === "function" && g.length === 1
   ? uncurry(g) 
   : g), f
);

const sum = uncurry(x => y => z => x + y + z);

sum(1, 2, 3); // 6
sum(1, 2)(3); // 6
sum(1)(2, 3); // 6

如果你喜欢自动取消柯里化,这个实现是可行的:一旦一个取消柯里化的函数本身产生一个柯里化函数作为 return 值,这个 returned 函数就会自动取消柯里化。如果你更喜欢更多的控制,下面的实现可能更合适。

最后的 uncurry 实现

const partial = arity => f => function _(...args) {
  return args.length < arity
   ? (...args_) => _(...args.concat(args_))
   : f(args);
};

const uncurry = arity => f => partial(arity)(args => args.reduce((g, x) => g(x), f));
const sum = uncurry(3)(x => y => z => x + y + z);

sum(1, 2, 3); // 6
sum(1, 2)(3); // 6
sum(1)(2, 3); // 6

这个微小的参数给我们带来了想要的控制。我觉得值得。

剩下的咖喱解决方案

我们如何处理超出我们控制范围且未被手动柯里化的函数?

const curryN = uncurry(2)(arity => f => partial(arity)(args => f(...args)));
const add = curryN(2, (x, y) => x + y);
const add2 = add(2);

add2(4); // 6

幸运的是,我们能够重用 partial 并保持 curryN 简洁。使用此解决方案,还可以柯里化带有可选参数的可变函数等。

奖励:"Funcualizing" 和柯里化方法

为了柯里化方法,我们需要将这个令人讨厌的隐式 this 属性 转换为显式参数。事实证明,我们可以再次使用 partial 来充分实现:

const apply = uncurry(2)(arity => key => {
  return arity
   ? partial(arity + 1)(args => args[arity][key](...args.slice(0, arity)))
   : o => o[key]();
});

apply(0, "toLowerCase")("A|B|C"); // "a|b|c"
apply(0, "toLowerCase", "A|B|C"); // "a|b|c"

apply(1, "split")("|")("A|B|C"); // ["A", "B", "C"]
apply(1, "split")("|", "A|B|C"); // ["A", "B", "C"]
apply(1, "split", "|", "A|B|C"); // ["A", "B", "C"]

apply(2, "includes")("A")(0)("A|B|C"); // true
apply(2, "includes", "A", 0, "A|B|C"); // true

在此 blog post 中详细讨论了柯里化。