柯里化和部分应用有什么区别?
-
03-07-2019 - |
题
我经常在互联网上看到各种抱怨,认为其他人的柯里化示例不是柯里化,而实际上只是部分应用。
我还没有找到关于什么是部分应用或者它与柯里化有何不同的合理解释。似乎存在普遍的混乱,等效的示例在某些地方被描述为柯里化,而在其他地方被描述为部分应用。
有人能给我提供这两个术语的定义以及它们有何不同的详细信息吗?
解决方案
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
。如果你只调用 fxy
或 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
其他提示
了解它们有何不同的最简单方法是考虑 真实的例子. 。假设我们有一个函数 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
柯里化 功能
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
换句话说,“柯里化”和“偏应用”是两个完全不同的函数。 柯里化仅需要 1 个输入,而部分应用则需要 2 个(或更多)输入。
尽管它们都返回一个函数作为输出,但返回的函数具有完全不同的形式,如上所示。
笔记:这是取自 F# 基础知识 这是一篇针对 .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 函数时,更准确地说是 假设部分应用了乘法函数。
乘法函数实际上是一系列两个函数。第一个 函数接受一个 int 参数并返回另一个函数, 有效地将 X 绑定到特定值。此函数还接受 一个 int 参数,您可以将其视为要绑定到 y 的值。后 调用第二个函数,x 和 y 都是绑定的,所以结果是 double 主体中定义的 x 和 y 的乘积。
为了创建双重,评估了乘法链中的第一个函数以部分应用乘法。最终的函数给出了名称double。当计算 double 时,它使用 它的参数以及部分应用的值来创建 结果。
有趣的问题。经过一些搜索后,“部分功能应用程序不会崩溃”给出了最佳解释我发现。我不能说实用的区别对我来说特别明显,但后来我不是FP专家......
另一个有用的页面(我承认我尚未完全阅读)是”使用Java闭包进行Currying和部分应用程序“。
看起来这是一个广泛混淆的术语,请注意。
我已在另一个主题 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。
Currying是 one 参数的函数,它接受函数 f
并返回一个新函数 h
。请注意, h
从 X
获取一个参数,并返回一个函数,它将
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)
双方将产生相同的单参数函数。
对于更高的arity函数,相等性不正确,因为在这种情况下currying将返回单参数函数,而部分应用程序将返回多参数函数。
差异也在于行为,而currying递归地转换整个原始函数(每个参数一次),部分应用只是一步替换。
来源: Wikipedia Currying 。
柯里化和部分应用之间的区别可以通过以下 JavaScript 示例得到最好的说明:
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)
, ,实际上并不是偏应用。
进一步阅读:
对我来说,部分应用程序必须创建一个新函数,其中使用的参数完全集成到结果函数中。
大多数函数式语言通过返回闭包来实现currying:在部分应用时不要在lambda下求值。因此,对于有趣的部分应用,我们需要在currying和部分应用之间做出区分,并将部分应用视为curda和lambda下的评估。
我在这里可能是非常错误的,因为我没有理论数学或函数式编程的强大背景,但是从我对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,点击“try coffeescript”。并粘贴在我的代码中以查看编译版本,这可能(希望)更有意义。谢谢!
我在学习的过程中经常遇到这个问题,并且多次被问到这个问题。我能描述差异的最简单方法是两者都相同:)让我解释一下......显然存在差异。
部分应用和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(最终评估中的参数总数)传递给部分应用程序实现,这可能会将我的两个描述混淆成一个令人困惑的混乱......但在那时,两种技术在很大程度上是可以互换的。
简单回答
Curry:允许你调用一个函数,将它分成多个调用,每个调用提供一个参数。
部分:允许您调用函数,将其拆分为多个调用,每次调用提供多个参数。
简单提示
两者都允许你调用一个提供较少参数的函数(或者更好,累积地提供它们)。实际上它们都(在每次调用时)将特定值绑定到函数的特定参数。
当函数有两个以上的参数时,可以看到真正的区别。
简单e(c)(样本)
(在Javascript中)
function process(context, success_callback, error_callback, subject) {...}
为什么总是传递参数,比如上下文和回调,如果它们总是一样的话?只需为函数
绑定一些值processSubject = _.partial(process, my_context, my_success, my_error)
并使用
在 subject1 和 foobar 上调用它processSubject('subject1');
processSubject('foobar');
舒服,不是吗? &#128521;
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');
声明
我跳过所有的学术/数学解释。因为我不知道。也许它有帮助&#128579;
这里还有其他很好的答案,但我相信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 );
}
在写这篇文章时,我混淆了currying和uncurrying。它们是函数的逆变换。只要你得到转换及其逆代表的东西,你所谓的无关紧要。
没有非常明确地定义(或者更确切地说,存在“冲突的”定义,这些定义都捕获了该想法的精神)。基本上,它意味着将一个带有多个参数的函数转换为一个带有单个参数的函数。例如,
(+) :: Int -> Int -> Int
现在,您如何将此转换为一个只需一个参数的函数?当然,你作弊!
plus :: (Int, Int) -> Int
请注意,plus现在只需要一个参数(由两件事组成)。超强!
这有什么意义?好吧,如果你有一个带有两个参数的函数,并且你有一对参数,那么很高兴知道你可以将函数应用于参数,并且仍然得到你期望的结果。而且,实际上,执行它的管道已经存在,因此您不必执行显式模式匹配等操作。您所要做的就是:
(uncurry (+)) (1,2)
那么什么是部分功能应用?将两个参数中的函数转换为具有一个参数的函数是一种不同的方法。它的工作方式不同。再说一次,我们以(+)为例。我们怎样才能将它变成一个以单个Int作为参数的函数?我们作弊!
((+) 0) :: Int -> Int
这是为任何Int添加零的函数。
((+) 1) :: Int -> Int
为任何Int添加1。等等。在这些情况中的每一种情况下,(+)被“部分地应用”。
我会假设大多数提出这个问题的人已经熟悉了基本概念,所以他们不需要谈论这个问题。重叠是令人困惑的部分。
您可能能够充分利用这些概念,但您将它们理解为这种伪原子无定形概念模糊。缺少的是知道它们之间的边界在哪里。
不是定义每个人的内容,而是更容易突出他们的差异和边界。
Currying 是指定义函数。
部分申请是致电的时候。
应用程序用于调用函数。
部分应用程序需要调用curried函数并将函数作为返回类型。