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

时间:2008-10-20 10:41:12

标签: language-agnostic terminology definition currying partial-application

我经常在互联网上看到各种各样的抱怨,其他人的讨论的例子并没有说明,但实际上只是部分应用。

我没有找到关于部分应用是什么的正确解释,或者它与currying有何不同。似乎存在普遍的混淆,在某些地方将等效的例子描述为currying,在其他地方描述为部分应用。

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

15 个答案:

答案 0 :(得分:226)

Currying将 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 y 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

答案 1 :(得分:146)

了解它们之间差异的最简单方法是考虑一个真实示例。让我们假设我们有一个函数Add,它将2个数字作为输入并返回一个数字作为输出,例如Add(7, 5)返回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
    
  • Currying 函数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"和"部分申请"是两个完全不同的功能。 Currying只需1个输入,而部分应用需要2个(或更多)输入。

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

答案 2 :(得分:48)

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

  

Currying意味着将具有多个参数的函数分解为一系列   每个函数都采用一个参数并最终生成   与原始功能相同的结果。 Currying可能是最多的   对于功能性编程新手的开发人员来说具有挑战性的主题   经常与部分应用混淆。你可以在工作中看到两者   在这个例子中:

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
     

通常,人们错误地认为乘法是形成双重的。   但这只是有点真实。乘法函数是curry,但是   定义它时会发生这种情况,因为F#中的函数是由咖喱的   默认。创建双重功能时,它更准确   说多重函数是部分应用的。

     

乘法函数实际上是一系列两个函数。首先   function接受一个int参数并返回另一个函数,   有效地将x绑定到特定值。此功能也接受   一个int参数,您可以将其视为绑定到y的值。后   调用第二个函数,x和y都绑定,所以结果是   x和y的乘积,如双体的定义。

     

创建double,乘法链中的第一个函数   评估函数以部分应用乘法。所结果的   函数名为double。当评估double时,它使用   它的参数以及部分应用的值来创建   结果

答案 3 :(得分:27)

有趣的问题。经过一番搜索,"Partial Function Application is not currying"给出了我发现的最佳解释。我不能说实用的区别对我来说特别明显,但后来我不是FP专家......

另一个有用的页面(我承认我尚未完全阅读)是"Currying and Partial Application with Java Closures"

看起来这是一个广泛混淆的术语,请注意。

答案 4 :(得分:12)

我已在另一个帖子https://stackoverflow.com/a/12846865/1685865中回答了这个问题。简而言之,部分函数应用程序是关于修复给定多变量函数的一些参数以产生具有较少参数的另一个函数,而Currying是关于将N个参数的函数转换为返回一元函数的一元函数... [示例在这篇文章的最后显示了Currying。]

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

(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

为了Currying。

答案 5 :(得分:6)

Currying是 one 参数的函数,它接受函数f并返回一个新函数h。请注意hX获取参数并返回将Y映射到Z函数

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

部分应用程序是两个(或更多)参数的函数,它接受函数ff的一个或多个附加参数并返回一个新函数{{ 1}}:

g

出现混淆是因为使用双参数函数时,以下等式成立:

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

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

对于更高的arity函数,相等性不正确,因为在这种情况下currying将返回单参数函数,而部分应用程序将返回多参数函数。

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

来源:Wikipedia Currying

答案 6 :(得分:5)

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

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

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

6 === partial(2, 3);

部分应用导致更小的功能;在上面的示例中,f的arity为3,而partial的arity为2.更重要的是,部分应用的函数会在调用时立即返回结果,而不是currying链中的另一个功能。因此,如果您看到partial(2)(3)之类的内容,那么实际上并不是部分应用。

进一步阅读:

答案 7 :(得分:2)

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

大多数函数式语言通过返回闭包来实现currying:在部分应用时不要在lambda下求值。因此,对于有趣的部分应用,我们需要在currying和部分应用之间做出区分,并将部分应用视为curda和lambda下的评估。

答案 8 :(得分:2)

我在这里可能是非常错误的,因为我没有理论数学或函数式编程的强大背景,但是从我对FP的短暂尝试,似乎currying倾向于将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个参数之前。

同样,这是我从我读过的东西中获取的。如果有人不同意,我会赞赏为什么而不是立即downvote的评论。此外,如果CoffeeScript难以阅读,请访问coffeescript.org,单击“尝试coffeescript”并粘贴我的代码以查看编译版本,这可能(希望)更有意义。谢谢!

答案 9 :(得分:2)

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

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

答案 10 :(得分:2)

简单答案

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

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


简单提示

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

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


简单的e(c)(样本)

(用Java语言编写)

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

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

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

并通过

subject1 foobar 上调用它

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

免责声明

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

答案 11 :(得分: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给你一个单参数函数来创建函数,其中partial-application创建一个硬编码一个或多个参数的包装函数。

如果你想复制和粘贴,以下是吵闹的,但更友好,因为类型更宽松:

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

答案 12 :(得分:1)

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

简单答案

固化::可以调用一个函数,将其拆分为多个调用,每次调用提供一个参数。

部分应用程序::可以调用一个函数,将其拆分为多个调用,每个调用提供多个参数。

两者之间的重大区别之一是对a的调用 部分应用的函数立即返回结果,而不是另一个 沿流通链运作;这种区别可以说明 对于Arity大于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 = partialAndCurry(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);

命名约定

我会在有空的时候写这个,这很快。

答案 13 :(得分:0)

在写这篇文章时,我混淆了currying和uncurrying。它们是函数的逆变换。只要你得到转换及其逆代表的东西,你所谓的无关紧要。

没有明确定义不公开(或者更确切地说,存在“冲突”的定义,这些定义都捕捉到了这个想法的精神)。基本上,它意味着将一个带有多个参数的函数转换为一个带有单个参数的函数。例如,

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

现在,您如何将此转换为一个只需一个参数的函数?当然,你作弊!

plus :: (Int, Int) -> Int

请注意,plus现在只需要一个参数(由两件事组成)。超强!

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

(uncurry (+)) (1,2)

那么什么是部分功能应用?将两个参数中的函数转换为具有一个参数的函数是一种不同的方法。它的工作方式不同。再说一次,我们以(+)为例。我们怎样才能将它变成一个以单个Int作为参数的函数?我们作弊!

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

这是为任何Int添加零的函数。

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

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

答案 14 :(得分:0)

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

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

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

固化是您定义该功能的时间。

部分应用是您调用该功能的时间。

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

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