【问题标题】:What is the difference between currying and partial application?柯里化和部分应用有什么区别?
【发布时间】:2010-09-18 02:18:55
【问题描述】:

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

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

谁能给我两个术语的定义,以及它们之间的区别细节?

【问题讨论】:

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


    【解决方案1】:

    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 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
    

    【讨论】:

    • 你是说部分应用是当你对一个函数进行柯里化,并使用一些,但不是所有的结果函数?
    • 或多或少,是的。如果你只提供参数的一个子集,你会得到一个接受其余参数的函数
    • 将函数 f(a, b, c, d) 更改为 g(a, b) 算作部分应用吗?还是仅在应用于柯里化函数时?很抱歉让您感到痛苦,但我在这里寻求明确的答案。
    • @Mark:我想这只是那些让我觉得书呆子的概念之一——但对权威来源的诉求并不能令人满意,因为它们似乎都指向彼此。维基百科几乎不是我认为的权威来源,但我知道很难找到更多其他内容。我只想说我认为我们都知道我们所说的内容及其力量,无论我们是否可以同意(或不同意)白话的细节! :) 谢谢马克!
    • @JasonBunting,关于你的第一条评论,你说的是decurrying。 Currying 将一个多参数函数作为输入,并返回一个 1-arg 函数链作为输出。去柯里化是将一串 1-arg 函数作为输入,并返回一个多 arg 函数作为输出。如stackoverflow.com/a/23438430/632951所述
    【解决方案2】:

    了解它们有何不同的最简单方法是考虑一个真实示例。假设我们有一个函数Add,它接受两个数字作为输入并返回一个数字作为输出,例如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
      
    • 柯里化函数Add会给我们一个新函数作为输出。该函数本身将 1 个数字作为输入并输出 yet 另一个新函数。然后,第三个函数将 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 个(或更多)输入。

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

    【讨论】:

    • 部分应用程序将函数从n-ary 转换为(x - n)-ary,从n-ary 柯里化到n * 1-ary。部分应用的函数具有(应用的)缩小范围,也就是说,Add7 的表达能力不如Add。另一方面,柯里化函数与原始函数一样富有表现力。
    • 我相信更显着的特点是当我们对 f(x,y,z)=>R 进行 curry 时,我们得到 f(x),它返回 g(y)=>h(z)=> R,每个都使用一个参数;但是当我们将 f(x,y,z) 部分应用为 f(x) 时,我们得到 g(y,z)=>R,即有两个参数。如果不是因为这个特性,我们可以说柯里化就像对 0 个参数的部分应用,因此所有参数都未绑定;但实际上 f() 部分应用于 0 个参数是一个一次消耗 3 个参数的函数,这与 curried f() 不同。
    • 再一次,正确的答案不是第一个或投票最多的:在这个答案的末尾简单解释 curry 与 partial 的签名确实是解决问题的最简单方法。
    • 评论f2(7)(5) is just a syntactic shortcut是什么意思? (我知道的很少。)f2 不是已经包含/“知道”7 了吗?
    • @Pacerier,在某处是否有 curry 实现(不要认为它在 functools 中)
    【解决方案3】:

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

    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
    

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

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

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

      • 第一个链接是关于差异的。这是我觉得有用的另一个:bit.ly/CurryingVersusPartialApplication
      • 柯里化与元组有关(将接受元组参数的函数转换为接受 n 个单独参数的函数,反之亦然)。部分应用是将函数应用于某些参数的能力,为剩余的参数产生一个新函数。如果您只是认为 currying == 与元组有关,那很容易记住。
      • 您发布的@Jon 链接提供了丰富的信息,但最好在此处扩展您的答案并添加更多信息。
      • 不敢相信你得到了 20 票赞成几个链接和承认你真的不知道咖喱和部分应用之间的区别。打得好,先生。
      • 有人生气他们不是 Jon Skeet
      【解决方案5】:

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

      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
      

      为了柯里化。

      【讨论】:

      • 你会说柯里化是部分应用的一个特定情况吗?
      • @SpoonMeiser,不,柯里化不是部分应用的特定情况:2 输入函数的部分应用与柯里化函数不同。见stackoverflow.com/a/23438430/632951
      【解决方案6】:

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

      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 将返回一个单参数函数,而部分应用程序将返回一个多参数函数。

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

      来源:Wikipedia Currying

      【讨论】:

        【解决方案7】:

        简单回答

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

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


        简单提示

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

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


        简单e(c)(示例)

        (在 Javascript 中)

        我们想在不同的subjects 上运行以下process 函数(例如,假设我们的主题是"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 返回一个预编译函数,可以使用缺少的参数调用一次并返回最终结果;
        • curry被多次调用(每个参数一个),每次都返回一个预编译的函数;除了使用最后一个参数调用的情况外,这将返回处理所有个参数的实际结果。

        使用 ES6:

        这是一个quick example,介绍了 ECMAScript 6 中的 Currying 和 Partial-application 的即时性。

        const curriedSum = math => eng => geo => math + eng + geo;
        const partialSum = math => (eng, geo) => math + eng + geo;
        

        【讨论】:

          【解决方案8】:

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

          function f(x, y, z) {
              return x + y + z;
          }
          
          var partial = f.bind(null, 1);
          
          6 === partial(2, 3);
          

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

          进一步阅读:

          【讨论】:

          • "一个部分应用的函数会在被调用时立即返回结果"——这是不正确的,是吗?当我部分应用一个函数时,该表达式返回一个函数,而不是“结果”。好的,您可能的意思是,当使用剩余的参数调用后一个函数时,它会返回结果,这与深入研究柯里化不同。但实际上没有人说您必须指定所有剩余的参数:您可以部分应用部分应用的结果,这将再次成为一个函数,而不是“结果”
          【解决方案9】:

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

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

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

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

          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
          

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

          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】:

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

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

            【讨论】:

              【解决方案11】:

              我在这里可能大错特错,因为我在理论数学或函数式编程方面没有很强的背景,但从我对 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”并粘贴我的代码以查看编译后的版本,这可能(希望)更有意义。谢谢!

              【讨论】:

                【解决方案12】:

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

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

                与其定义每一个是什么,不如只突出它们的区别——边界。

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

                部分应用是当你调用函数时。

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

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

                【讨论】:

                  【解决方案13】:

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

                  【讨论】:

                  • 以下内容为我提供了关键见解:“因此,柯里化为您提供了一个单参数函数来创建函数,其中部分应用程序创建了一个对一个或多个参数进行硬编码的包装函数。”
                  【解决方案14】:

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

                  简单回答

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

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

                  两者之间的显着区别之一是调用 部分应用的函数立即返回结果,而不是另一个 在柯里化链中发挥作用;这种区别可以说明 显然对于元数大于二的函数。

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

                  示例

                  部分应用和柯里化

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

                  命名约定

                  我有空就写这个,很快。

                  【讨论】:

                    【解决方案15】:

                    在写这篇文章时,我混淆了 currying 和 uncurrying。它们是函数的逆变换。你叫什么真的不重要,只要你知道变换和它的逆代表什么。

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

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

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

                    plus :: (Int, Int) -> Int
                    

                    请注意,plus 现在接受一个参数(由两件事组成)。超级棒!

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

                    (uncurry (+)) (1,2)
                    

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

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

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

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

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

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 2011-06-25
                      • 1970-01-01
                      • 2012-03-14
                      • 2012-12-27
                      • 2021-04-04
                      相关资源
                      最近更新 更多