473

我经常在网上看到各种各样的抱怨,说别人的柯里化例子不是柯里化,实际上只是部分应用。

对于什么是部分应用程序,或者它与柯里化有何不同,我还没有找到一个体面的解释。似乎存在一种普遍的混淆,等效的例子在某些地方被描述为柯里化,而在其他地方则被描述为部分应用。

有人可以为我提供这两个术语的定义,以及它们之间的区别细节吗?

4

15 回答 15

277

柯里化是将 n 个参数的单个函数转换n函数,每个函数都有一个参数。给定以下功能:

function f(x,y,z) { z(x(y));}

当咖喱,变成:

function f(x) { lambda(y) { lambda(z) { z(x(y)); } } }

为了得到 f(x,y,z) 的完整应用,你需要这样做:

f(x)(y)(z);

许多函数式语言让您编写f x y z. 如果你只调用f x yor f(x)(y)那么你会得到一个部分应用的函数——返回值是一个闭包,lambda(z){z(x(y))}其中传入了 x 和 y 的值f(x,y)

使用部分应用的一种方法是将函数定义为广义函数的部分应用,例如fold

function fold(combineFunction, accumulator, list) {/* ... */}
function sum     = curry(fold)(lambda(accum,e){e+accum}))(0);
function length  = curry(fold)(lambda(accum,_){1+accum})(empty-list);
function reverse = curry(fold)(lambda(accum,e){concat(e,accum)})(empty-list);

/* ... */
@list = [1, 2, 3, 4]
sum(list) //returns 10
@f = fold(lambda(accum,e){e+accum}) //f = lambda(accumulator,list) {/*...*/}
f(0,list) //returns 10
@g = f(0) //same as sum
g(list)  //returns 10
于 2008-10-20T11:02:57.453 回答
176

了解它们有何不同的最简单方法是考虑一个真实的例子。假设我们有一个函数Add,它以 2 个数字作为输入并返回一个数字作为输出,例如,Add(7, 5)returns 12。在这种情况下:

  • 部分应用Add具有值的函数7将为我们提供一个新函数作为输出。该函数本身将 1 个数字作为输入并输出一个数字。像这样:

    Partial(Add, 7); // returns a function f2 as output
    
                     // f2 takes 1 number as input and returns a number as output
    

    所以我们可以这样做:

    f2 = Partial(Add, 7);
    f2(5); // returns 12;
           // f2(7)(5) is just a syntactic shortcut
    
  • 对函数进行柯里Add化将为我们提供一个新函数作为输出。该函数本身将 1 个数字作为输入并输出另一个新函数。然后,第三个函数将 1 个数字作为输入并返回一个数字作为输出。像这样:

    Curry(Add); // returns a function f2 as output
    
                // f2 takes 1 number as input and returns a function f3 as output
                // i.e. f2(number) = f3
    
                // f3 takes 1 number as input and returns a number as output
                // i.e. f3(number) = number
    

    所以我们可以这样做:

    f2 = Curry(Add);
    f3 = f2(7);
    f3(5); // returns 12
    

换句话说,“currying”和“partial application”是两个完全不同的功能。柯里化只需要 1 个输入,而部分应用需要 2 个(或更多)输入。

尽管它们都返回一个函数作为输出,但返回的函数具有完全不同的形式,如上所示。

于 2014-05-02T23:26:31.313 回答
56

注意:这取自F# Basics一篇优秀的介绍性文章,供 .NET 开发人员进入函数式编程。

柯里化意味着将具有许多参数的函数分解为一系列函数,每个函数接受一个参数并最终产生与原始函数相同的结果。对于刚接触函数式编程的开发人员来说,柯里化可能是最具挑战性的话题,特别是因为它经常与部分应用程序混淆。您可以在此示例中看到两者都在起作用:

let multiply x y = x * y    
let double = multiply 2
let ten = double 5

马上,您应该会看到与大多数命令式语言不同的行为。第二条语句通过将一个参数传递给一个接受两个参数的函数来创建一个名为 double 的新函数。结果是一个函数,它接受一个 int 参数并产生相同的输出,就像您调用了 x 等于 2 和 y 等于该参数的乘法一样。在行为方面,它与此代码相同:

let double2 z = multiply 2 z

通常,人们错误地说乘法被柯里化以形成双精度。但这只是有点真实。乘法函数是柯里化的,但在定义它时会发生这种情况,因为 F# 中的函数默认是柯里化的。创建 double 函数时,更准确地说是部分应用了 multiply 函数。

乘法函数实际上是一系列两个函数。第一个函数接受一个 int 参数并返回另一个函数,有效地将 x 绑定到特定值。此函数还接受一个 int 参数,您可以将其视为绑定到 y 的值。调用第二个函数后,x 和 y 都绑定了,因此结果是 double 的主体中定义的 x 和 y 的乘积。

为了创建双精度,乘法函数链中的第一个函数被评估为部分应用乘法。生成的函数被命名为 double。当 double 被评估时,它使用它的参数以及部分应用的值来创建结果。

于 2012-05-04T05:18:33.213 回答
37

有趣的问题。经过一番搜索,“Partial Function Application is not currying”给出了我找到的最好的解释。我不能说实际差异对我来说特别明显,但我不是FP专家......

另一个看起来很有用的页面(我承认我还没有完全阅读)是“Currying and Partial Application with Java Closures”

请注意,这看起来确实是一对被广泛混淆的术语。

于 2008-10-20T11:02:06.737 回答
17

我已经在另一个线程https://stackoverflow.com/a/12846865/1685865中回答了这个问题。简而言之,偏函数应用是关于固定给定多变量函数的一些参数以产生另一个具有更少参数的函数,而 Currying 是将 N 个参数的函数转换为返回一元函数的一元函数...... [An example of柯里化显示在这篇文章的末尾。]

柯里化主要是理论上的兴趣:人们可以只使用一元函数来表达计算(即每个函数都是一元的)。在实践中,作为一种副产品,如果语言具有柯里化函数,它可以使许多有用的(但不是全部)部分功能应用程序变得微不足道。同样,它不是实现部分应用程序的唯一方法。因此,您可能会遇到以其他方式完成部分应用程序的情况,但人们将其误认为是 Currying。

(咖喱的例子)

在实践中,一个人不会只写

lambda x: lambda y: lambda z: x + y + z

或等效的 javascript

function (x) { return function (y){ return function (z){ return x + y + z }}}

代替

lambda x, y, z: x + y + z

为了咖喱。

于 2012-10-11T19:40:41.990 回答
12

柯里化是一个参数的函数,它接受一个函数f并返回一个新函数h。请注意,它h接受一个参数X并返回一个映射到的函数YZ

curry(f) = h 
f: (X x Y) -> Z 
h: X -> (Y -> Z)

部分应用是两个(或多个)参数的函数,它接受一个函数f和一个或多个附加参数f并返回一个新函数g

part(f, 2) = g
f: (X x Y) -> Z 
g: Y -> Z

之所以会产生混淆,是因为对于两个参数的函数,以下等式成立:

partial(f, a) = curry(f)(a)

双方将产生相同的单参数函数。

对于更高元数的函数,等式不成立,因为在这种情况下,currying 将返回一个单参数函数,而部分应用程序将返回一个多参数函数。

不同之处还在于行为,而柯里化递归地转换整个原始函数(每个参数一次),部分应用只是一步替换。

资料来源:维基百科咖喱

于 2017-08-30T07:52:22.830 回答
11

简单的答案

Curry:允许您调用一个函数,将其拆分为多个调用,每次调用提供一个参数。

部分:允许您调用一个函数,将其拆分为多个调用,每次调用提供多个参数。


简单提示

两者都允许您调用提供更少参数的函数(或者更好的是,累积提供它们)。实际上,它们都(在每次调用时)将特定值绑定到函数的特定参数。

当函数有超过 2 个参数时,可以看到真正的区别。


简单的 e(c)(样本)

(在 Javascript 中)

我们想process在不同的 s 上运行以下函数subject(例如,假设我们的主题是"subject1""foobar"字符串):

function process(context, successCallback, errorCallback, subject) {...}

为什么总是传递参数,比如上下文和回调,如果它们总是相同的?

只需为函数绑定一些值:

processSubject = _.partial(process, my_context, my_success, my_error)
// assign fixed values to the first 3 arguments of the `process` function

并在subject1foobar上调用它,省略前 3 个参数的重复,其中:

processSubject('subject1');
processSubject('foobar');

舒服,不是吗?


使用currying,您需要每次传递一个参数

curriedProcess = _.curry(process);   // make the function curry-able
processWithBoundedContext = curriedProcess(my_context);
processWithCallbacks = processWithBoundedContext(my_success)(my_error); // note: these are two sequential calls

result1 = processWithCallbacks('subject1');
// same as: process(my_context, my_success, my_error, 'subject1');

result2 = processWithCallbacks('foobar'); 
// same as: process(my_context, my_success, my_error, 'foobar');

免责声明

我跳过了所有的学术/数学解释。因为我不知道。也许它有帮助


编辑:

正如@basickarl所补充的,这两个函数的进一步细微差别(参见Lodash示例)是:

  • partial返回一个预编译函数,可以使用缺少的参数调用一次并返回最终结果;
  • whilecurry 被多次调用(每个参数一个),每次返回一个预先准备好的函数;除了使用最后一个参数调用的情况外,这将返回处理所有参数的实际结果。

使用 ES6:

这是 ECMAScript 6 中即时柯里化和部分应用程序的快速示例。

const curriedSum = math => eng => geo => math + eng + geo;
const partialSum = math => (eng, geo) => math + eng + geo;
于 2018-07-09T20:13:38.190 回答
10

通过以下 JavaScript 示例可以最好地说明 curry 和部分应用程序之间的区别:

function f(x, y, z) {
    return x + y + z;
}

var partial = f.bind(null, 1);

6 === partial(2, 3);

部分应用导致较小的arity函数;在上面的示例中, farity 为 3 而partial只有 arity 2。更重要的是,部分应用的函数将在被调用时立即返回结果,而不是沿着柯里化链的另一个函数。因此,如果您看到类似partial(2)(3)的内容,实际上它并不是部分应用。

进一步阅读:

于 2013-05-27T03:51:08.067 回答
5

我在学习的过程中经常遇到这个问题,并且被问过很多次。我可以描述差异的最简单方法是两者都是相同的:) 让我解释一下……显然存在差异。

部分应用和柯里化都涉及为函数提供参数,可能不是一次全部。一个相当典型的例子是添加两个数字。在伪代码(实际上是没有关键字的JS)中,基函数可能如下:

add = (x, y) => x + y

如果我想要一个“addOne”函数,我可以部分应用它或咖喱它:

addOneC = curry(add, 1)
addOneP = partial(add, 1)

现在使用它们很清楚:

addOneC(2) #=> 3
addOneP(2) #=> 3

那么有什么区别呢?好吧,这很微妙,但部分应用程序涉及提供一些参数,然后返回的函数将在下一次调用时执行主函数,而柯里化将继续等待,直到它拥有所有必要的参数:

curriedAdd = curry(add) # notice, no args are provided
addOne = curriedAdd(1) # returns a function that can be used to provide the last argument
addOne(2) #=> returns 3, as we want

partialAdd = partial(add) # no args provided, but this still returns a function
addOne = partialAdd(1) # oops! can only use a partially applied function once, so now we're trying to add one to an undefined value (no second argument), and we get an error

简而言之,使用部分应用预填充一些值,知道下次调用该方法时,它会执行,留下 undefined 所有未提供的参数;当您想要根据需要多次返回部分应用的函数以完成函数签名时,请使用柯里化。最后一个人为的例子:

curriedAdd = curry(add)
curriedAdd()()()()()(1)(2) # ugly and dumb, but it works

partialAdd = partial(add)
partialAdd()()()()()(1)(2) # second invocation of those 7 calls fires it off with undefined parameters

希望这可以帮助!

更新:某些语言或lib实现将允许您将arity(最终评估中的参数总数)传递给部分应用程序实现,这可能会将我的两个描述混为一谈......但在这一点上,这两种技术是很大程度上可以互换。

于 2017-11-13T02:30:17.730 回答
4

对我来说,部分应用程序必须创建一个新函数,其中使用的参数完全集成到结果函数中。

大多数函数式语言通过返回闭包来实现柯里化:部分应用时不要在 lambda 下进行评估。因此,为了让部分应用变得有趣,我们需要区分柯里化和部分应用,并将部分应用视为 lambda 下的柯里化加求值。

于 2013-05-13T10:03:40.747 回答
4

我在这里可能错了,因为我在理论数学或函数式编程方面没有很强的背景,但是从我对 FP 的短暂尝试来看,似乎柯里化倾向于将 N 个参数的函数转换为一个参数的 N 个函数,而偏应用 [在实践中] 更适用于具有不确定数量参数的可变参数函数。我知道以前答案中的一些示例不符合这种解释,但它对我区分概念的帮助最大。考虑这个例子(为简洁起见,用 CoffeeScript 编写,如果进一步混淆,我深表歉意,但如果需要,请要求澄清):

# partial application
partial_apply = (func) ->
  args = [].slice.call arguments, 1
  -> func.apply null, args.concat [].slice.call arguments

sum_variadic = -> [].reduce.call arguments, (acc, num) -> acc + num

add_to_7_and_5 = partial_apply sum_variadic, 7, 5

add_to_7_and_5 10 # returns 22
add_to_7_and_5 10, 11, 12 # returns 45

# currying
curry = (func) ->
  num_args = func.length
  helper = (prev) ->
    ->
      args = prev.concat [].slice.call arguments
      return if args.length < num_args then helper args else func.apply null, args
  helper []

sum_of_three = (x, y, z) -> x + y + z
curried_sum_of_three = curry sum_of_three
curried_sum_of_three 4 # returns a function expecting more arguments
curried_sum_of_three(4)(5) # still returns a function expecting more arguments
curried_sum_of_three(4)(5)(6) # returns 15
curried_sum_of_three 4, 5, 6 # returns 15

这显然是一个人为的例子,但请注意,部分应用一个接受任意数量参数的函数允许我们执行一个函数,但需要一些初步数据。对函数进行柯里化是类似的,但它允许我们分段执行 N 参数函数,直到但仅在所有 N 个参数都被考虑在内。

同样,这是我从我读过的东西中得到的。如果有人不同意,我会很感激评论为什么而不是立即投反对票。此外,如果 CoffeeScript 难以阅读,请访问 coffeescript.org,单击“try coffeescript”并粘贴我的代码以查看编译后的版本,这可能(希望)更有意义。谢谢!

于 2015-12-07T05:00:23.313 回答
4

我假设大多数问这个问题的人已经熟悉基本概念,所以他们没有必要谈论这个。令人困惑的是重叠部分。

您可能能够充分使用这些概念,但您将它们一起理解为这种伪原子无定形概念模糊。缺少的是知道它们之间的边界在哪里。

与其定义每一个是什么,更容易突出它们的差异——边界。

柯里化是你定义函数的时候。

部分应用程序是在您调用该函数时。

应用程序是调用函数的数学语言。

部分应用程序需要调用一个柯里化函数并获取一个函数作为返回类型。

于 2019-05-12T19:11:29.637 回答
3

这里还有其他很好的答案,但我相信这个 Java 示例(根据我的理解)可能对某些人有益:

public static <A,B,X> Function< B, X > partiallyApply( BiFunction< A, B, X > aBiFunction, A aValue ){
    return b -> aBiFunction.apply( aValue, b );
}

public static <A,X> Supplier< X > partiallyApply( Function< A, X > aFunction, A aValue ){
    return () -> aFunction.apply( aValue );
}

public static <A,B,X> Function<  A, Function< B, X >  > curry( BiFunction< A, B, X > bif ){
    return a -> partiallyApply( bif, a );
}

因此,柯里化为您提供了一个单参数函数来创建函数,其中部分应用程序创建一个包装函数,该函数对一个或多个参数进行硬编码。

如果您想复制和粘贴,以下内容更嘈杂但更友好,因为类型更宽松:

public static <A,B,X> Function< ? super B, ? extends X > partiallyApply( final BiFunction< ? super A, ? super B, X > aBiFunction, final A aValue ){
    return b -> aBiFunction.apply( aValue, b );
}

public static <A,X> Supplier< ? extends X > partiallyApply( final Function< ? super A, X > aFunction, final A aValue ){
    return () -> aFunction.apply( aValue );
}

public static <A,B,X> Function<  ? super A,  Function< ? super B, ? extends X >  > curry( final BiFunction< ? super A, ? super B, ? extends X > bif ){
    return a -> partiallyApply( bif, a );
}
于 2017-02-27T21:19:31.123 回答
2

这里的很多人都没有正确解决这个问题,也没有人谈论过重叠。

简单的答案

Currying:让您调用一个函数,将其拆分为多个调用,每次调用提供一个参数。

部分应用程序:允许您调用一个函数,将其拆分为多个调用,每次调用提供多个参数。

两者之间的显着差异之一是调用部分应用的函数会立即返回结果,而不是柯里化链中的另一个函数。对于元数大于二的函数,可以清楚地说明这种区别。

这意味着什么?这意味着最多有两次调用偏函数。柯里化的参数数量与数量一样多。如果柯里化函数只有两个参数,那么它本质上与偏函数相同。

例子

部分应用和柯里化

function bothPartialAndCurry(firstArgument) {
    return function(secondArgument) {
        return firstArgument + secondArgument;
    }
}

const partialAndCurry = bothPartialAndCurry(1);
const result = partialAndCurry(2);

部分申请

function partialOnly(firstArgument, secondArgument) {
    return function(thirdArgument, fourthArgument, fifthArgument) {
        return firstArgument + secondArgument + thirdArgument + fourthArgument + fifthArgument;
    }
}

const partial = partialOnly(1, 2);
const result = partial(3, 4, 5);

咖喱

function curryOnly(firstArgument) {
    return function(secondArgument) {
        return function(thirdArgument) {
            return function(fourthArgument ) {
                return function(fifthArgument) {
                    return firstArgument + secondArgument + thirdArgument + fourthArgument + fifthArgument;
                }
            }
        }
    }
}

const curryFirst = curryOnly(1);
const currySecond = curryFirst(2);
const curryThird = currySecond(3);
const curryFourth = curryThird(4);
const result = curryFourth(5);

// or...

const result = curryOnly(1)(2)(3)(4)(5);

命名约定

有时间我会写这个,很快。

于 2020-10-21T16:17:44.807 回答
1

在写这篇文章时,我混淆了 currying 和 uncurrying。它们是函数的逆变换。只要你得到变换及其逆表示的内容,你称之为什么并不重要。

Uncurrying 的定义不是很清楚(或者更确切地说,有一些“冲突”的定义都抓住了这个想法的精神)。基本上,这意味着将一个接受多个参数的函数转换为一个接受单个参数的函数。例如,

(+) :: Int -> Int -> Int

现在,你如何把它变成一个接受单个参数的函数?你作弊,当然!

plus :: (Int, Int) -> Int

请注意, plus 现在接受一个参数(由两件事组成)。极好的!

这有什么意义?好吧,如果您有一个带有两个参数的函数,并且您有一对参数,那么很高兴知道您可以将函数应用于参数,并且仍然可以得到您期望的结果。而且,事实上,实现它的管道已经存在,因此您不必执行诸如显式模式匹配之类的事情。你所要做的就是:

(uncurry (+)) (1,2)

那么什么是偏函数应用呢?将具有两个参数的函数转换为具有一个参数的函数是另一种方法。但它的工作方式不同。同样,让我们​​以(+)为例。我们如何将它变成一个以单个 Int 作为参数的函数?我们作弊!

((+) 0) :: Int -> Int

这就是将零添加到任何 Int 的函数。

((+) 1) :: Int -> Int

任何 Int 加 1。等等。在每种情况下,(+) 都是“部分应用”。

于 2014-04-09T03:29:17.213 回答