currying和部分应用有什么区别?


438

我经常在Internet上看到各种抱怨,认为其他人使用的currying实例不是curying,而实际上只是部分应用。

我尚未找到关于什么是部分应用程序或它与currying有何不同的恰当解释。似乎存在一个普遍的混乱,在某些地方将等效示例描述为“ currying”,而在其他地方则将其部分应用。

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

Answers:


256

咖喱化是将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 yf(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

40
您是说部分应用程序是当您咖喱函数并使用部分但不是全部结果函数时?
SpoonMeiser

9
或多或少,是的。如果您仅提供参数的一个子集,您将获得一个接受其余参数的函数
Mark Cidade

1
将函数f(a,b,c,d)更改为g(a,b)会算作部分应用吗?还是仅适用于咖喱函数?抱歉,很痛苦,但是我想在这里寻求一个明确的答案。
SpoonMeiser

2
@Mark:我想这只是那些在我心中浮出水面的概念中的一个-但是吸引权威人士的想法并不能令人满意,因为它们似乎彼此指向对方。维基百科几乎不是我认为的权威来源,但是我知道很难找到更多的东西。只需说一句,我认为我们都知道我们所说的内容及其力量,而不管我们是否可以就白话的细节达成一致(或不同意)!:)谢谢马克!
杰森·邦廷

5
@JasonBunting,关于您的第一个评论,您所谈论的是递归的。Currying将multi-arg函数作为输入,并返回1-arg函数的链作为输出。De-currying将一串1-arg函数作为输入,并返回一个multi-arg函数作为输出。作为上阐述stackoverflow.com/a/23438430/632951
Pacerier

164

要了解它们之间的差异,最简单的方法是考虑一个真实的例子。假设我们有一个函数Add以2个数字作为输入并返回一个数字作为输出,例如Add(7, 5)return 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号作为输入,并输出另一个新的功能。然后,该第三个函数将一个数字作为输入,并返回一个数字作为输出。因此:

    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个(或更多)输入。

即使它们都返回一个函数作为输出,返回的函数也具有完全不同的形式,如上所述。


24
部分应用程序将功能从转换n-ary(x - n)-ary,从转换n-aryn * 1-ary。部分应用的函数的范围有所缩小(即,Add7比)的表达性差Add。另一方面,咖喱函数与原始函数一样具有表现力。
鲍勃

4
我相信更鲜明的特征是当我们咖喱f(x,y,z)=> R时,我们得到f(x),它返回g(y)=> h(z)=> R,每个消耗一个参数;但是当我们将f(x,y,z)部分地应用为f(x)时,我们得到g(y,z)=> R,即带有两个参数。如果不是因为这个特性,我们可以说currying就像部分应用到0个参数,从而使所有参数不受约束;但是实际上,部分f()部分应用于0参数是一次消耗3个args的函数,这与咖喱f()不同。
Maksim Gumerov

2
再一次,正确的答案不是最先投票的还是投票最多的:在答案的最后简单说明咖喱或部分咖喱的签名确实是解决问题的最简单方法。
fnl

2
评论f2(7)(5) is just a syntactic shortcut是什么意思?(我知道的很少。)还不f2包含/“知道” 7吗?
Zach Mierzejewski

@Pacerier,curry某处有实现(不要认为它存在functools
alancalvitti,

51

注意:这摘自F#基础知识,这是.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函数时,更准确地说乘函数是部分应用的。

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

要创建双精度,对乘法函数链中的第一个函数求值以部分应用乘法。结果函数的名称为double。对double求值时,它将使用其参数以及部分应用的值来创建结果。


33

有趣的问题。经过一番搜索,“部分函数应用程序不发生问题”给出了我发现的最佳解释。我不能说实际的区别对我来说特别明显,但是那时我不是FP专家...

另一个看上去有用的页面(我承认我还没有完全阅读过)是“使用Java闭包实现部分和局部应用程序”

请注意,这看起来确实是一个很容易混淆的术语。


5
第一个链接是关于差异的。这是我发现有用的另一个:bit.ly/CurryingVersusPartialApplication
Jason Bunting 2010年

5
咖喱化与元组有关(将采用元组参数的函数转换为采用n个独立参数的函数,反之亦然)。部分应用是将功能应用于某些参数的功能,可为其余参数产生一个新功能。很容易记住,如果您只是认为与元组相关联的==。
Don Stewart

9
您发布的@Jon链接提供了很多信息,但是最好扩展您的答案并在此处添加更多信息。
Zaheer Ahmed 2014年


11
简直不敢相信您获得了20个赞成情侣链接的票数,而且您真的不知道咖喱和部分申请之间的区别。先生,打得好。
AlienWebguy

16

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

咖喱化主要是理论上的兴趣:人们只能使用一元函数来表示计算(即每个函数都是一元的)。在实践中,作为一种副产品,如果该语言具有咖喱函数,那么它可以使许多有用的(但不是全部)部分功能应用变得微不足道。同样,它不是实现部分应用程序的唯一方法。因此,您可能会遇到以其他方式完成部分应用程序但人们误以为是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

为了咖喱。


1
那您会说currying是部分申请的一种具体情况吗?
SpoonMeiser 2012年

1
@SpoonMeiser,否,currying不是部分应用程序的特定情况:2输入函数的部分应用程序与curring该函数不同。请参阅stackoverflow.com/a/23438430/632951
Pacerier

10

咖喱是一个参数的函数,它接受一个函数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)

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

较高的Arity函数的相等性不成立,因为在这种情况下currying将返回一个单参数函数,而部分应用程序将返回一个多参数函数。

区别还在于行为,而currying递归地转换整个原始函数(每个参数一次),而部分应用只是一步替换。

资料来源:维基百科


8

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

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

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

6 === partial(2, 3);

部分应用会导致功能降低。在上面的示例中,f具有3的Aritypartial只有2的偶数。更重要的是,部分应用的函数将在被调用时立即返回结果,而不是当前链中的另一个函数。因此,如果您看到类似的信息partial(2)(3),那么实际上这并不是部分应用。

进一步阅读:


“部分应用的函数将在被调用时立即返回结果” –是不正确的?当我部分应用一个函数时,该表达式返回一个函数,而不是“结果”。好的,您可能是说后面的函数在与其余参数一起调用时会返回结果,这与深入分析currying不同。但实际上没有人说您必须指定所有剩余的参数:您可以部分应用部分应用的结果,这将再次是一个函数,而不是“结果”
Maksim Gumerov

6

简单的答案

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

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


简单提示

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

当函数具有两个以上的参数时,可以看到真正的区别。


简单e(c)(样本)

(使用Javascript)

function process(context, success_callback, error_callback, subject) {...}

为什么总是传递参数,例如上下文和回调,如果它们总是相同的呢?只需为函数绑定一些值

processSubject = _.partial(process, my_context, my_success, my_error)

并调用它subject1foob​​ar的

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

舒服,不是吗?😉

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

curriedProcess = _.curry(process);
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');

免责声明

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


4

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

部分应用程序和currying都涉及为函数提供参数,也许不是一次全部提供。一个相当典型的例子是将两个数字相加。在伪代码(实际上是不带关键字的JS)中,基本函数可能如下:

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

如果我想要一个“ addOne”功能,则可以部分应用或使用它:

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

现在使用它们很明显:

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

那有什么区别呢?好吧,这很微妙,但是部分应用程序需要提供一些参数,然后返回的函数将在下次调用时执行main函数,而currying将一直等待直到拥有所有必需的参数为止:

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

简而言之,请使用部分应用程序来预填充一些值,因为您知道下次您调用该方法时,它将执行,未定义的所有未提供的参数都将保留;当您想要连续返回部分应用的函数以完成函数签名所需的次数时,请使用currying。最后一个人为的例子:

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实现将允许您将部分参数(最终评估中的参数总数)传递给部分应用实现,这可能会将我的两个描述混淆成一个混乱的混乱……但是,到那时,这两种技术是在很大程度上可以互换。


3

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

大多数功能语言都通过返回闭包来实现currying:部分应用时,不要在lambda下求值。因此,为了使部分应用程序有趣,我们需要区分当前应用程序和部分应用程序,并将部分应用程序视为lambda下的当前应用程序和评估。


3

我在这里可能是非常错误的,因为我没有理论数学或函数式编程方面的深厚背景,但是从我对FP的短暂探究看来,curry倾向于将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,单击“尝试coffeescript”并粘贴到我的代码中以查看编译后的版本,这可能(希望)更有意义。谢谢!


2

我将假设大多数提出这个问题的人已经熟悉基本概念,因此他们无需讨论。重叠是令人困惑的部分。

您也许可以完全使用这些概念,但是您可以将它们理解为伪原子的无定形概念模糊。缺少的是知道它们之间的边界在哪里。

与其定义每个对象是什么,不如仅突出显示它们之间的差异(边界),这更容易。

定义函数时就是咖喱

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

应用程序在调用函数方面讲数学。

部分应用程序需要调用curried函数并获取函数作为返回类型。


1

这里还有其他很好的答案,但我相信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 );
}

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

如果要复制和粘贴,以下内容会比较吵杂,但友好,因为类型更宽松:

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

以下是我的主要见解:“因此,currying为您提供了一个用于创建函数的单参数函数,其中部分应用程序创建了用于对一个或多个参数进行硬编码的包装函数。”
罗兰

0

在写这篇文章时,我混淆了粗俗和粗俗。它们是函数的逆变换。只要得到转换及其逆表示的内容,您实际上将其称为什么并不重要。

没有清楚地定义单义性(或者确切地说,有“冲突”的定义都抓住了这个思想的精神)。基本上,这意味着将带有多个参数的函数转换为带有单个参数的函数。例如,

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

现在,如何将其转换为带有单个参数的函数?你骗,当然!

plus :: (Int, Int) -> Int

注意,plus现在带有一个参数(由两部分组成)。超!

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

(uncurry (+)) (1,2)

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

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

那就是给任何Int加零的函数。

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.