如何正确柯里化 JavaScript 中的函数?

How to correctly curry a function in JavaScript?

我在 JavaScript 中写了一个简单的 curry 函数,它在大多数情况下都能正常工作:

const add = curry((a, b, c) => a + b + c);

const add2 = add(2);

const add5 = add2(3);

console.log(add5(5));
<script>
const curried = Symbol("curried");

Object.defineProperty(curry, curried, { value: true });

function curry(functor, ...initArgs) {
    if (arguments.length === 0) return curry;

    if (typeof functor !== "function") {
        const value = JSON.stringify(functor);
        throw new TypeError(`${value} is not a function`);
    }

    if (functor[curried] || initArgs.length >= functor.length)
        return functor(...initArgs);

    const result = (...restArgs) => curry(functor, ...initArgs, ...restArgs);

    return Object.defineProperty(result, curried, { value: true });
}
</script>

但是,它不适用于以下情况:

// length :: [a] -> Number
const length = a => a.length;

// filter :: (a -> Bool) -> [a] -> [a]
const filter = curry((f, a) => a.filter(f));

// compose :: (b -> c) -> (a -> b) -> a -> c
const compose = curry((f, g, x) => f(g(x)));

// countWhere :: (a -> Bool) -> [a] -> Number
const countWhere = compose(compose(length), filter);

根据下面的问题countWhere定义为(length .) . filter

What does (f .) . g mean in Haskell?

因此我应该可以使用 countWhere 如下:

const odd = n => n % 2 === 1;

countWhere(odd, [1,2,3,4,5]);

但是,它没有返回 3(数组的长度 [1,3,5]),而是 returns 一个函数。我做错了什么?

您的 curry 函数(以及 JavaScript 中的 most curry functions that people write)的问题是它不能正确处理额外的参数。

curry 的作用

假设 f 是一个函数并且 f.lengthn。设 curry(f)g。我们用 m 个参数调用 g。应该发生什么?

  1. 如果m === 0那么就return g.
  2. If m < n then partially apply f to the m new arguments, and return a new curried function that accepts the remaining n - m arguments.
  3. 否则将 f 应用于 m 参数和 return 结果。

这是大多数 curry 函数所做的,但这是错误的。前两种情况是对的,但第三种情况是错误的。相反,它应该是:

  1. 如果m === 0那么就return g.
  2. If m < n then partially apply f to the m new arguments, and return a new curried function that accepts the remaining n - m arguments.
  3. 如果 m === n 则将 f 应用于 m 参数。如果结果是一个函数,那么 curry 结果。最后,return结果。
  4. 如果 m > n 则将 f 应用于前 n 个参数。如果结果是一个函数,那么 curry 结果。最后,将结果应用于剩余的 m - n 个参数和 return 新结果。

大多数curry函数的问题

考虑以下代码:

const countWhere = compose(compose(length), filter);

countWhere(odd, [1,2,3,4,5]);

如果我们使用了不正确的 curry 函数,那么这相当于:

compose(compose(length), filter, odd, [1,2,3,4,5]);

然而,compose 只接受三个参数。删除最后一个参数:

const compose = curry((f, g, x) =>f(g(x)));

因此,上述表达式的计算结果为:

compose(length)(filter(odd));

这进一步评估为:

compose(length, filter(odd));

compose 函数需要一个参数,这就是为什么它 return 是一个函数而不是 returning 3 的原因。要获得正确的输出,您需要编写:

countWhere(odd)([1,2,3,4,5]);

这就是大多数curry函数出错的原因。

使用正确curry函数的解决方案

再次考虑以下代码:

const countWhere = compose(compose(length), filter);

countWhere(odd, [1,2,3,4,5]);

如果我们使用正确的curry函数,那么这相当于:

compose(compose(length), filter, odd)([1,2,3,4,5]);

计算结果为:

compose(length)(filter(odd))([1,2,3,4,5]);

进一步评估为(跳过中间步骤):

compose(length, filter(odd), [1,2,3,4,5]);

这导致:

length(filter(odd, [1,2,3,4,5]));

产生正确的结果3

执行正确的curry函数

在 ES6 中实现正确的 curry 函数很简单:

const curried = Symbol("curried");

Object.defineProperty(curry, curried, { value: true });

function curry(functor, ...initArgs) {
    if (arguments.length === 0) return curry;

    if (typeof functor !== "function") {
        const value = JSON.stringify(functor);
        throw new TypeError(`${value} is not a function`);
    }

    if (functor[curried]) return functor(...initArgs);

    const arity = functor.length;
    const args = initArgs.length;

    if (args >= arity) {
        const result = functor(...initArgs.slice(0, arity));
        return typeof result === "function" || args > arity ?
            curry(result, ...initArgs.slice(arity)) : result;
    }

    const result = (...restArgs) => curry(functor, ...initArgs, ...restArgs);

    return Object.defineProperty(result, curried, { value: true });
}

我不确定 curry 的实现有多快。也许有人可以让它更快。

使用正确 curry 函数的意义

使用正确的curry函数可以直接将Haskell代码翻译成JavaScript。例如:

const id = curry(a => a);

const flip = curry((f, x, y) => f(y, x));

id 函数很有用,因为它允许您轻松地部分应用非柯里化函数:

const add = (a, b) => a + b;

const add2 = id(add, 2);

flip 函数很有用,因为它可以让您轻松地在 JavaScript 中创建 right sections:

const sub = (a, b) => a - b;

const sub2 = flip(sub, 2); // equivalent to (x - 2)

这也意味着您不需要像这样的技巧extended compose function:

What's a Good Name for this extended `compose` function?

你可以简单地写:

const project = compose(map, pick);

如问题中所述,如果要组合 lengthfilter,则使用 (f .) . g 模式:

What does (f .) . g mean in Haskell?

另一个解决方案是创建高阶 compose 函数:

const compose2 = compose(compose, compose);

const countWhere = compose2(length, fitler);

这一切都是可能的,因为 curry 函数的正确实现。

深思

当我想组成一个函数链时,我通常使用下面的chain函数:

const chain = compose((a, x) => {
    var length = a.length;
    while (length > 0) x = a[--length](x);
    return x;
});

这允许您编写如下代码:

const inc = add(1);

const foo = chain([map(inc), filter(odd), take(5)]);

foo([1,2,3,4,5,6,7,8,9,10]); // [2,4,6]

相当于下面的Haskell代码:

let foo = map (+1) . filter odd . take 5

foo [1,2,3,4,5,6,7,8,9,10]

它还允许您编写如下代码:

chain([map(inc), filter(odd), take(5)], [1,2,3,4,5,6,7,8,9,10]); // [2,4,6]

相当于下面的Haskell代码:

map (+1) . filter odd . take 5 $ [1,2,3,4,5,6,7,8,9,10]

希望对您有所帮助。

@Aadit,

我 post 这样做是因为你分享了我对 的回答的评论 我没有在其中专门讨论柯里化 post 因为这是一个非常有争议的话题并且我真的不想在那里打开一罐蠕虫。

我会小心使用“如何正确地咖喱”当你似乎在添加自己的糖分和便利时进入您的实施。

无论如何,撇开所有这些不谈,我真的不打算将其设为 argumentative/combative post。我希望能够在 JavaScript 中就柯里化进行公开、友好的讨论,同时强调我们方法之间的一些差异。

事不宜迟...


澄清:

给定 f 是一个函数,f.lengthn。设 curry(f)g。我们用 m 个参数调用 g。应该发生什么?你说:

  1. If m === 0 then just return g.
  2. If m < n then partially apply f to the m new arguments, and return a new curried function which accepts the remaining n - m arguments.
  3. If m === n then apply f to the m arguments. If the result is a function then curry the result. Finally, return the result.
  4. If m > n then apply f to the first n arguments. If the result is a function then curry the result. Finally, apply the result to the remaining m - n arguments and return the new result.

让我们看看 的代码实际做了什么的代码示例

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

var z = add(1, 2, 3);
console.log(z(4)); // 10

这里发生了两件事:

  1. 您正在尝试支持使用可变参数调用柯里化函数。
  2. 您正在自动柯里化 returned 函数

我认为这里没有太多争论的余地,但人们似乎忽略了 currying 实际上是什么

via: Wikipedia
In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument...

我将最后一点加粗,因为它非常重要;序列中的每个函数只接受一个单个参数;不像您建议的那样可变参数(0、1 或更多)。

你在 post 中也提到了 haskell,所以我假设你知道 Haskell 没有接受多个参数的函数。 (注意:接受一个元组的函数仍然只是一个接受一个参数的函数,一个元组)。这样做的原因是深刻的,并为您提供了具有可变参数的函数无法提供的表达方式的灵活性。

那么让我们重新问原来的问题:应该发生什么?

好吧,当每个函数只接受 1 个参数时,这很简单。在任何时候,如果给出的参数超过 1 个,它们就会被删除。

function id(x) {
  return x;
}

当我们调用 id(1,2,3,4) 时会发生什么?当然我们只返回 12,3,4 完全被忽略了。这是:

  1. JavaScript 的工作原理
  2. 维基百科如何说柯里化应该起作用
  3. 我们应该如何实施我们自己的 curry 解决方案

Before we go further, I'm going to use ES6-style arrow functions but I will also include the ES5 equivalent at the bottom of this post. (Probably later tonight.)

另一种柯里化技术

在这种方法中,我们编写了一个 curry 函数,该函数连续 returns 单参数函数,直到指定了所有参数

作为这个实现的结果,我们有 6 个多功能函数。

// no nonsense curry
const curry = f => {
  const aux = (n, xs) =>
    n === 0 ? f (...xs) : x => aux (n - 1, [...xs, x])
  return aux (f.length, [])
}
   
// demo
let sum3 = curry(function(x,y,z) {
  return x + y + z;
});
    
console.log (sum3 (3) (5) (-1)); // 7

好的,我们已经看到了使用简单辅助循环实现的 curry 技术。它没有依赖项,并且有一个不到 5 行代码的声明性定义。它允许函数被部分应用,一次 1 个参数,就像咖喱函数应该工作一样。

没有魔法,没有不可预见的自动柯里化,没有其他不可预见的后果。


但是柯里化到底有什么意义呢?

好吧,事实证明,我并没有真正 curry 我编写的功能。正如您在下面看到的,我通常以柯里化形式定义所有可重用函数。所以真的,当你想与一些你无法控制的功能交互时,你只需要 curry ,可能来自 lib 或其他东西;其中一些可能具有可变接口!

我送curryN

// the more versatile, curryN
const curryN = n => f => {
  const aux = (n, xs) =>
    n === 0 ? f (...xs) : x => aux (n - 1, [...xs, x])
  return aux (n, [])
};

// curry derived from curryN
const curry = f => curryN (f.length) (f);

// some caveman function
let sumN = function() {
  return [].slice.call(arguments).reduce(function(a, b) {
    return a + b;
  });
};

// curry a fixed number of arguments
let g = curryN (5) (sumN);
console.log (g (1) (2) (3) (4) (5)); // 15


咖喱还是不咖喱?就是这个问题

我们将编写一些示例,其中我们的函数都是柯里化形式。功能将保持极其简单。每个都有 1 参数,每个都有一个 return 表达式。

// composing two functions
const comp = f => g => x => f (g (x))
const mod  = y => x => x % y
const eq   = y => x => x === y
const odd  = comp (eq (1)) (mod (2))

console.log (odd(1)) // true
console.log (odd(2)) // false

你的countWhere函数

// comp :: (b -> c) -> (a -> b) -> (a -> c)
const comp = f => g => x =>
  f(g(x))

// mod :: Int -> Int -> Int
const mod = x => y =>
  y % x

// type Comparable = Number | String
// eq :: Comparable -> Comparable -> Boolean
const eq = x => y =>
  y === x

// odd :: Int -> Boolean
const odd =
  comp (eq(1)) (mod(2))

// reduce :: (b -> a -> b) -> b -> ([a]) -> b
const reduce = f => y => ([x,...xs]) =>
  x === undefined ? y : reduce (f) (f(y)(x)) (xs)

// filter :: (a -> Boolean) -> [a] -> [a]
const filter = f =>
  reduce (acc => x => f (x) ? [...acc,x] : acc) ([])

// length :: [a] -> Int
const length = x =>
  x.length

// countWhere :: (a -> Boolean) -> [a] -> Int
const countWhere = f =>
  comp (length) (filter(f));

console.log (countWhere (odd) ([1,2,3,4,5]))
// 3


备注

那么咖喱还是不咖喱?

// to curry
const add3 = curry((a, b, c) =>
  a + b + c
)

// not to curry
const add3 = a => b => c =>
 a + b + c

随着 ES6 箭头函数成为当今 JavaScripter 的首选,我认为手动柯里化函数的选择是显而易见的。它实际上更短,而且以柯里化形式写出来的开销也更少。

就是说,您仍将与不提供所公开函数的柯里化形式的库进行交互。对于这种情况,我建议

  • currycurryN(定义如上)
  • partial(如defined here

@Iven,

您的 curryN 实施非常好。此部分专为您而设。

const U = f=> f (f)
const Y = U (h=> f=> f(x=> h (h) (f) (x)))

const curryN = Y (h=> xs=> n=> f=>
  n === 0 ? f(...xs) : x=> h ([...xs, x]) (n-1) (f)
) ([])

const curry = f=> curryN (f.length) (f)

const add3 = curry ((x,y,z)=> x + y + z)

console .log (add3 (3) (6) (9))

//---Currying refers to copying a function but with preset parameters

function multiply(a,b){return a*b};

var productOfSixNFiveSix = multiply.bind(this,6,5);

console.log(productOfSixNFive());

//The same can be done using apply() and call()

var productOfSixNFiveSix = multiply.call(this,6,5);

console.log(productOfSixNFive);

var productOfSixNFiveSix = multiply.apply(this,[6,5]);

console.log(productOfSixNFive);

除了它的数学定义

currying is the transformation of a function with n parameters into a sequence of n functions, which each accept a single parameter. The arity is thus transformed from n-ary to n * 1-ary

柯里化对编程有什么影响? 对数量的抽象!

const comp = f => g => x => f(g(x));
const inc = x => x + 1;
const mul = y => x => x * y;
const sqr = x => mul(x)(x);

comp(sqr)(inc)(1); // 4
comp(mul)(inc)(1)(2); // 4

comp 需要两个函数 fg 以及一个任意参数 x。因此 g 必须是一元函数(只有一个形式参数的函数)并且 f 也是,因为它被提供 g 的 return 值。 comp(sqr)(inc)(1) 有效不会让任何人感到惊讶。 sqrinc 都是一元的。

但是mul显然是二元函数。这到底是怎么回事?因为柯里化抽象了 mul 的元数。你现在大概可以想象柯里化是一个多么强大的特性了。

在 ES2015 中,我们可以简洁地使用箭头函数预柯里化我们的函数:

const map = (f, acc = []) => xs => xs.length > 0
 ? map(f, [...acc, f(xs[0])])(xs.slice(1))
 : acc;

map(x => x + 1)([1,2,3]); // [2,3,4]

然而,我们需要一个程序化的 curry 函数来处理我们无法控制的所有函数。因为我们了解到柯里化主要意味着对元数的抽象,所以我们的实现 不能依赖于 Function.length:

const curryN = (n, acc = []) => f => x => n > 1
 ? curryN(n - 1, [...acc, x])(f)
 : f(...acc, x);

const map = (f, xs) => xs.map(x => f(x));
curryN(2)(map)(x => x + 1)([1,2,3]); // [2,3,4]

将 arity 显式传递给 curryN 具有很好的副作用,我们也可以柯里化可变参数函数:

const sum = (...args) => args.reduce((acc, x) => acc + x, 0);
curryN(3)(sum)(1)(2)(3); // 6

仍然存在一个问题:我们的 curry 解决方案无法处理方法。好的,我们可以轻松地重新定义我们需要的方法:

const concat = ys => xs => xs.concat(ys);
const append = x => concat([x]);

concat([4])([1,2,3]); // [1,2,3,4]
append([4])([1,2,3]); // [1,2,3,[4]]

另一种方法是以可以处理多参数函数和方法的方式调整 curryN

const curryN = (n, acc = []) => f => x => n > 1 
 ? curryN(n - 1, [...acc, x])(f)
 : typeof f === "function"
  ? f(...acc, x)
  : x[f](...acc);

curryN(2)("concat")(4)([1,2,3]); // [1,2,3,4]

虽然我不知道这是否是在 Javascript 中柯里化函数(和方法)的正确方法。这是一种可能的方式。

编辑:

naomik 指出,通过使用默认值,curry 函数的内部 API 会部分暴露。因此,curry 函数的简化是以牺牲其稳定性为代价的。为了避免 API 泄漏,我们需要一个包装函数。我们可以利用 U 组合器(类似于 naomik 的 Y 解决方案):

const U = f => f(f);
const curryN = U(h => acc => n => f => x => n > 1
 ? h(h)([...acc, x])(n-1)(f)
 : f(...acc, x))([]);

缺点:该实现更难阅读并且会降低性能。