currying有什么好处?


154

我刚刚学习了有关curry的知识,尽管我认为我理解了curry的概念,但在使用它方面并没有看到任何大的优势。

作为一个简单的示例,我使用一个将两个值相加的函数(用ML编写)。该版本不使用curry

fun add(x, y) = x + y

并称为

add(3, 5)

而咖喱版本是

fun add x y = x + y 
(* short for val add = fn x => fn y=> x + y *)

并称为

add 3 5

在我看来,这只是语法糖,它从定义和调用函数中除去了一组括号。我已经看到currying被列为功能语言的重要功能之一,但现在我对此有点不知所措。创建只使用每个参数的函数链而不是使用元组的函数的概念对于语法的简单更改而言似乎相当复杂。

稍微简单的语法是否是引起混乱的唯一动机,还是我错过了一些在我非常简单的示例中并不明显的其他优点?只是语法糖吗?


54
单独进行咖喱本质上是没有用的,但是默认情况下让所有函数咖喱都会使许多其他功能更易于使用。在您实际使用功能语言一段时间之前,很难理解这一点。
CA McCann

4
delnan在评论JoelEtherton的答案时提到的一件事是,但我想我要明确提到的是,(至少在Haskell中)您不仅可以部分地应用函数,而且可以使用类型构造函数-这可以相当便利; 这可能是需要考虑的事情。
保罗

所有人都给出了Haskell的示例。也许有人会想,仅在Haskell中,currying才有用。
Manoj R

@ManojR 在Haskell中都没有给出示例。
phwd

1
这个问题引起了关于Reddit的相当有趣的讨论。
yannis

Answers:


126

通过使用咖喱函数,您可以更加专心地重用更多抽象函数。假设您有一个添加功能

add x y = x + y

并且您要向列表的每个成员添加2。在Haskell中,您可以这样做:

map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific

与必须创建函数相比,此处的语法更轻松 add2

add2 y = add 2 y
map add2 [1, 2, 3]

或者,如果您必须执行匿名lambda函数:

map (\y -> 2 + y) [1, 2, 3]

它还允许您从不同的实现中抽象出来。假设您有两个查找功能。一个来自键/值对列表,一个指向值的键,另一个来自映射,从键到值以及一个键到值,如下所示:

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

然后,您可以创建一个接受从键到值的查找功能的函数。您可以将上面的任何查找功能(分别部分应用于列表或映射)传递给它:

myFunc :: (Key -> Value) -> .....

总结:currying很好,因为它使您可以使用轻量级语法专门化/部分应用函数,然后将这些部分应用的函数传递给诸如map或的高阶函数filter。高阶函数(将函数作为参数或将其作为结果)是函数式编程的基础,而currying和部分应用的函数使高阶函数的使用更加有效和简洁。


31
值得注意的是,因此,Haskell中用于函数的参数顺序通常基于部分应用的可能性,这又使上述好处在更多情况下适用(ha,ha)。因此,默认情况下进行咖喱比从此处的具体示例所看到的更为有益。
CA McCann

at “一个来自键/值对列表和一个键,指向一个值,另一个来自映射关系,从键到值以及一个键到一个值”
Mateen Ulhaq

@MateenUlhaq这是前一句话的延续,我想我们想基于一个键来获取一个值,并且我们有两种方法可以做到这一点。该句子列举了这两种方式。第一种方法是为您提供键/值对的列表以及我们要为其查找值的键,第二种方法是为您提供适当的映射,然后再提供一个键。紧接句子后面的代码可能会有所帮助。
鲍里斯(Boris)

53

实际的答案是,使用currying使创建匿名函数更加容易。即使使用最小的lambda语法,这也是一个胜利。相比:

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

如果您使用的lambda语法很丑陋,那就更糟了。(我在看着您,JavaScript,Scheme和Python。)

随着您使用越来越多的高阶函数,这变得越来越有用。虽然我用更多的在Haskell的高阶函数比在其他语言中,我发现我实际使用的lambda语法,因为类似的时间的三分之二,拉姆达也只是一个部分应用功能。(还有很多其他时间,我将其提取到命名函数中。)

从根本上说,函数的哪个版本是“规范的”并不总是很明显。例如,以map。的类型map可以通过两种方式编写:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

哪个是“正确”的?实际上很难说。实际上,大多数语言都使用第一种语言-映射接受一个函数和一个列表,然后返回一个列表。但是,从根本上讲,map实际上所做的是将正常函数映射到列表函数-它接受一个函数并返回一个函数。如果map是咖喱,则您不必回答这个问题:它以一种非常优雅的方式完成了这两个任务

一旦您概括map为列表以外的其他类型,这就变得尤为重要。

另外,计算确实不是很复杂。实际上,与大多数语言使用的模型相比,它有点简化:您不需要将任何包含在您的语言中的多个参数的函数的概念表示出来。这也更紧密地反映了潜在的lambda演算。

当然,ML风格的语言没有咖喱或非咖喱形式的多个参数的概念。该f(a, b, c)语法实际上对应于传入元组(a, b, c)f,所以f仍只对参数。我实际上希望这是其他语言所具有的非常有用的区别,因为它使编写类似以下内容变得很自然:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

对于带有多个参数的语言,您很难做到这一点!


1
“ ML风格的语言没有咖喱或非咖喱形式的多个参数的概念”:就此而言,Haskell ML风格是吗?
Giorgio

1
@乔治:是的。
Tikhon Jelvis

1
有趣。我认识一些Haskell,并且我正在学习SML,因此有趣的是,看到了这两种语言之间的异同。
Giorgio

很好的答案,如果您仍然不相信,那就考虑一下类似于lambda流的Unix管道
Sridhar Sarnobat,2016年

“实用”的答案无关紧要,因为冗长的用法通常是通过部分应用而不是逐句地避免来避免。我在这里认为,lambda抽象的语法(尽管有类型声明)比Scheme中的(至少)丑陋,因为它需要更多内置的特殊语法规则来正确解析它,这使语言规范肿而没有任何收获关于语义属性。
FrankHB

24

如果您有一个传递为第一类对象的函数,并且没有在代码中的某个地方收到评估它所需的所有参数,则Currying可能很有用。您可以在获得一个或多个参数时简单地应用它们,然后将结果传递给具有更多参数的另一段代码,并在那里完成评估。

比起需要首先将所有参数汇总在一起的代码,此代码将更简单。

此外,还有可能会更多地重复使用代码,因为带有单个参数的函数(另一个咖喱函数)不必专门针对所有参数进行匹配。


14

(至少在最初时)进行欺骗的主要动机不是实践而是理论。特别是,使用currying可以有效地获取多参数函数,而无需实际为其定义语义或为产品定义语义。这导致了一种具有与另一种更复杂的语言一样多的表现力的简单语言,因此是人们所希望的。


2
虽然这里的动机是理论上的,但我认为简单性几乎总是一个实践优势。不用担心多参数函数会使我的编程工作变得更轻松,就像使用语义一样。
迪洪·耶尔维斯

2
@TikhonJelvis但是,在编程时,currying还使您担心其他事情,例如编译器无法捕捉到您向函数传递的参数太少的事实,或者在这种情况下甚至得到了错误的错误消息。当您不使用currying时,错误会更加明显。
Alex R

我从来没有遇到过这样的问题:至少,GHC在这方面非常出色。编译器始终会捕获此类问题,并且对于该错误也具有良好的错误消息。
迪洪·耶尔维斯

1
我不同意错误消息的质量。可以维护,是的,但是还不够好。它也只会在导致类型错误的情况下捕获此类问题,即,如果您以后尝试将结果用作函数以外的其他功能(或者您已对类型进行注释,但是依靠它来读取可读错误有其自身的问题) ); 错误的报告位置与实际位置相离。
Alex R

14

(我将在Haskell中举例说明。)

  1. 使用函数式语言时,可以部分应用函数非常方便。就像在Haskell的(== x)函数中一样,True如果其参数等于给定项,则该函数将返回x

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    无需粗心大意,我们的可读性就会有所降低:

    mem x lst = any (\y -> y == x) lst
    
  2. 这与默认编程有关(另请参见Haskell Wiki上的Pointfree样式)。这种样式不关注变量表示的值,而是关注组成函数以及信息如何通过函数链流动。我们可以将示例转换为完全不使用变量的形式:

    mem = any . (==)
    

    在这里,我们看到==的功能从aa -> Boolany为从功能a -> Bool[a] -> Bool。通过简单地组成它们,我们得到了结果。这全都归功于curring。

  3. 在某些情况下,相反的,不费吹灰之力也很有用。例如,假设我们要将一个列表分为两部分-小于10的元素和其余部分,然后将这两个列表连接起来。列表的拆分是通过(在这里我们也使用curried )完成的。结果为类型。取而代之的提取结果到其第一和第二部分,它们使用相结合的,我们可以直接uncurrying做到这一点的partition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

的确(uncurry (++) . partition (< 10)) [4,12,11,1][4,1,12,11]

在理论上也有重要的优点:

  1. 对于缺少数据类型且仅具有功能的语言(例如lambda演算),咖喱化至关重要。尽管这些语言对实际使用没有用,但从理论上讲它们非常重要。
  2. 这与功能语言的基本属性有关-功能是一流的对象。如我们所见,从(a, b) -> c到的转换a -> (b -> c)意味着后一个函数的结果是type b -> c。换句话说,结果是一个函数。
  3. (Un)currying与笛卡尔封闭类别密切相关,这是查看键入的λ结石的一种分类方式。

对于“可读性差得多的代码”位,不是mem x lst = any (\y -> y == x) lst吗?(带有反斜杠)。
stusmith

是的,谢谢您指出这一点,我会予以纠正。
PetrPudlák13年

9

咖喱不仅仅是语法糖!

考虑add1(uncurried)和add2(curried)的类型签名:

add1 : (int * int) -> int
add2 : int -> (int -> int)

(在两种情况下,类型签名中的括号都是可选的,但为清楚起见,我将其包括在内。)

add1是一个包含2个元组的intint并返回的函数intadd2是一个接受int并返回另一个函数,随后接受int并返回的函数int

当我们明确指定函数应用程序时,两者之间的本质区别变得更加明显。让我们定义一个函数(非咖喱函数),将其第一个参数应用于第二个参数:

apply(f, b) = f b

现在我们可以看到的区别add1add2更清晰。 add1通过2元组调用:

apply(add1, (3, 5))

但是用add2调用,int 然后用另一个调用其返回值int

apply(apply(add2, 3), 5)

编辑: currying的本质好处是您可以免费获得部分申请。假设您想要一个类型为5的函数int -> int(例如,map在列表中)。您可以编写代码addFiveToParam x = x+5,也可以使用内联lambda进行等效操作,但是您也可以更轻松地写(尤其是在这种情况下琐碎的情况下)add2 5


3
我知道我的示例在幕后有很大的不同,但是结果似乎是一个简单的语法更改。
疯狂的科学家

5
咖喱并不是一个很深的概念。它是关于简化基础模型(请参阅Lambda演算)或使用具有元组的语言,实际上是关于部分应用程序的语法便利性。不要低估语法便利性的重要性。
Peaker

9

咖喱只是语法糖,但我认为您对糖的作用有些误解。以你为例

fun add x y = x + y

实际上是语法糖

fun add x = fn y => x + y

也就是说,(add x)返回一个接受参数y的函数,并将x加到y上。

fun addTuple (x, y) = x + y

这是一个接受元组并添加其元素的函数。这两个功能实际上是完全不同的。他们采取不同的论点。

如果要在列表中的所有数字上加2:

(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]

结果将是[3,4,5]

另一方面,如果要对列表中的每个元组求和,则addTuple函数非常合适。

(* Sum each tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]    
(* sum each tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]

结果将是[12,13,14]

在部分应用有用的地方,咖喱函数非常有用-例如地图,折叠,应用,过滤器。考虑一下此函数,该函数返回提供的列表中的最大正数;如果没有正数,则返回0:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 

1
我确实了解到curried函数具有不同的类型签名,并且它实际上是一个返回另一个函数的函数。我虽然缺少部分应用程序部分。
疯狂的科学家

9

我还没有看到的另一件事是,柯里化允许(有限)抽象化而不是Arity。

考虑这些功能是Haskell库的一部分

(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

在每种情况下,类型变量c都可以是函数类型,以便这些函数在其参数的参数列表的某些前缀上工作。无需花费时间,您要么需要特殊的语言功能来抽象函数arity,要么具有专门针对不同arities的这些函数的许多不同版本。


6

我有限的理解是这样的:

1)部分功能应用

部分函数应用程序是返回带有较少数量参数的函数的过程。如果您提供3个参数中的2个,它将返回一个接受3-2 = 1个参数的函数。如果您提供3个参数中的1个,它将返回一个带有3-1 = 2个参数的函数。如果需要,甚至可以部分应用3个参数中的3个,它将返回不带参数的函数。

因此,给出以下功能:

f(x,y,z) = x + y + z;

当将1绑定到x并将其部分应用于上述函数时,f(x,y,z)您将得到:

f(1,y,z) = f'(y,z);

哪里: f'(y,z) = 1 + y + z;

现在,如果将y绑定到2并将z绑定到3,然后部分应用,f'(y,z)您将得到:

f'(2,3) = f''();

其中:f''() = 1 + 2 + 3;

现在,在任何时候,你可以选择评估ff'f''。所以我可以做:

print(f''()) // and it would return 6;

要么

print(f'(1,1)) // and it would return 3;

2)咖喱

另一方面,咖喱是将一个函数拆分为一个包含一个参数函数的嵌套链的过程。您永远不能提供超过1个参数,它是一或零。

因此给定相同的功能:

f(x,y,z) = x + y + z;

如果您对它进行管理,将获得3个功能链:

f'(x) -> f''(y) -> f'''(z)

哪里:

f'(x) = x + f''(y);

f''(y) = y + f'''(z);

f'''(z) = z;

现在,如果你打电话f'(x)x = 1

f'(1) = 1 + f''(y);

返回一个新函数:

g(y) = 1 + f''(y);

如果调用g(y)具有y = 2

g(2) = 1 + 2 + f'''(z);

返回一个新函数:

h(z) = 1 + 2 + f'''(z);

最后,如果你调用h(z)z = 3

h(3) = 1 + 2 + 3;

您已返回6

3)关闭

最后,闭包是将功能和数据作为单个单元一起捕获的过程。函数闭包可以接受0到无限数量的参数,但是它也知道没有传递给它的数据。

同样,给定相同的功能:

f(x,y,z) = x + y + z;

您可以改写闭包:

f(x) = x + f'(y, z);

哪里:

f'(y,z) = x + y + z;

f'已关闭x。意思是f'可以读取里面的x的值f

所以,如果你是打电话fx = 1

f(1) = 1 + f'(y, z);

您将获得关闭:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

现在,如果您closureOfF使用y = 2和致电z = 3

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 3;

哪个会回来 6

结论

库里函数,部分应用程序和闭包在某种程度上都相似,因为它们将函数分解为更多部分。

Currying将多个参数的函数分解为单个参数的嵌套函数,这些嵌套函数返回单个参数的函数。引入一个或更少参数的函数是没有意义的,因为这没有意义。

部分应用程序将多个自变量的函数分解为较小自变量的函数,该函数现在将缺少的自变量替换为提供的值。

闭包将一个函数分解为一个函数和一个数据集,其中未传递的函数内变量可以在数据集内查找以求值以供求值时绑定。

所有这些令人困惑的是,它们可以被用来实现其他子集的一种。因此,从本质上讲,它们只是实现细节。它们都提供了相似的值,因为您不需要预先收集所有值,并且可以重用部分函数,​​因为您已经将其分解为谨慎的单元。

揭露

我绝不是该主题的专家,我只是最近才开始学习这些知识,因此,我提供了我目前的理解,但是可能有一些错误,请您指出,我将按/如果我发现了。


1
因此答案是:欺骗没有优势?
ceving

1
@ceving据我所知,这是正确的。在实践中,currying和部分应用将为您带来相同的好处。选择哪种语言来实现是出于实现的原因,一个给定的语言可能比另一个容易实现。
Didier A.

5

通过Currying(部分应用程序),您可以通过固定一些参数从现有功能中创建新功能。这是词汇闭包的一种特殊情况,其中匿名函数只是一个琐碎的包装器,它将一些捕获的参数传递给另一个函数。我们也可以通过使用通用语法来进行词法闭包来做到这一点,但是部分应用程序提供了简化的语法糖。

这就是为什么Lisp程序员以功能性样式工作时有时会使用库来进行部分应用程序

而不是(lambda (x) (+ 3 x))给我们提供一个在参数(op + 3)中加3的函数,您可以编写类似的代码,然后在某列表的每个元素上加3 (mapcar (op + 3) some-list)而不是(mapcar (lambda (x) (+ 3 x)) some-list)。此op宏将使您成为一个接受一些参数x y z ...并调用的函数(+ a x y z ...)

在许多纯函数式语言中,部分应用程序已根植于语法中,因此没有op运算符。要触发部分应用程序,您只需调用一个参数少于其所需参数的函数即可。结果不会产生"insufficient number of arguments"错误,而是其余参数的函数。


“通过...可以通过固定一些参数来创建新函数...”-否,类型为的函数a -> b -> c没有参数s(复数),它只有一个参数c。调用时,它将返回type函数a -> b
Max Heiber

4

对于功能

fun add(x, y) = x + y

它的形式 f': 'a * 'b -> 'c

评价一个人会做

add(3, 5)
val it = 8 : int

对于咖喱功能

fun add x y = x + y

评价一个人会做

add 3
val it = fn : int -> int

如果是部分计算,特别是(3 + y),则可以用

it 5
val it = 8 : int

在第二种情况下添加形式为 f: 'a -> 'b -> 'c

在这里,currying所做的是将一个将需要两个协议的函数转换为只需要一个返回结果的函数。部分评估

为什么有人需要这个?

x在RHS上说的不只是常规的int,而是复杂的计算,需要花费一些时间才能完成,例如,需要两秒钟。

x = twoSecondsComputation(z)

所以功能现在看起来像

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

类型 add : int * int -> int

现在我们要为一系列数字计算此函数,让我们对其进行映射

val result1 = map (fn x => add (20, x)) [3, 5, 7];

对于上述结果,twoSecondsComputation每一次都会评估的结果。这意味着此计算需要6秒钟。

结合使用分段和循环可以避免这种情况。

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

咖喱形式 add : int -> int -> int

现在可以做到的

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

twoSecondsComputation只需要进行一次评估。要放大比例,请用15分钟(或任何小时)替换两秒,然后针对100个数字进行映射。

简介:与其他方法一起用于较高级别的功能(作为部分评估的工具)时,咖喱很棒。它的目的本身无法真正证明。


3

固化允许灵活的功能组合。

我组成了一个函数“ curry”。在这种情况下,我不在乎获得哪种记录器或它来自何处。我不在乎动作是什么或它来自哪里。我只关心处理输入。

var builder = curry(function(input, logger, action) {
     logger.log("Starting action");
     try {
         action(input);
         logger.log("Success!");
     }
     catch (err) {
         logger.logerror("Boo we failed..", err);
     }
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.

builder变量是一个函数,该函数返回一个函数,该函数返回一个函数,该函数接受执行我的工作的输入。这是一个简单有用的示例,而不是可见的对象。


2

当您没有函数的所有参数时,使用Currying是一个优点。如果碰巧完全评估了该功能,则没有明显的区别。

使用Currying,您可以避免提及尚未需要的参数。它更加简洁,不需要查找与范围内的另一个变量不冲突的参数名称(这是我最喜欢的好处)。

例如,当使用以函数作为参数的函数时,通常会遇到需要“将3加到输入”或“将输入与变量v比较”之类的情况。借助curring,这些函数很容易编写:add 3(== v)。您必须使用lambda表达式:x => add 3 xx => x == v。Lambda表达式的长度是它的两倍,并且除了x已经存在x作用域之外,还有少量忙于挑选名称的工作。

基于currying的语言的一个附带好处是,在为函数编写通用代码时,最终不会基于参数的数量产生数百种变体。例如,在C#中,“ curry”方法将需要Func <R>,Func <A,R>,Func <A1,A2,R>,Func <A1,A2,A3,R>等的变体永远。在Haskell中,Func <A1,A2,R>的等效项更像Func <Tuple <A1,A2>,R>或Func <A1,Func <A2,R >>(和Func <R>更像是Func <Unit,R>),因此所有变体都对应于单个Func <A,R>的情况。


2

我可以想到的主要推理(无论如何我都不是这个主题的专家)开始显示它的好处,因为功能从琐碎的变为琐碎的。在具有这种性质的大多数概念的所有琐碎情况下,您都不会发现真正的好处。但是,大多数功能语言在处理操作中都大量使用堆栈。以PostScriptLisp为例。通过使用curring,可以更有效地堆叠功能,并且随着操作的增长越来越小,这种好处变得显而易见。以咖喱的方式,命令和参数可以按顺序扔到堆栈上,并根据需要弹出,以便它们以正确的顺序运行。


1
究竟需要多少堆栈框架才能使工作效率更高?
梅森惠勒

1
@MasonWheeler:我不知道,因为我说我不是函数语言方面的专家,也不是专门从事具体的语言方面的专家。因此,我标记了这个社区Wiki。
乔尔·埃瑟顿

4
@MasonWheeler您对这个答案的措词有一点了解,但让我插话说,实际上创建的堆栈帧数量在很大程度上取决于实现。例如,在无脊椎无标签G机(STG; GHC实现Haskell的方式)中,延迟实际评估,直到它累积了所有(或至少需要知道的数量)参数为止。我似乎不记得是对所有函数还是仅对构造函数执行此操作,但是我认为大多数函数应该都可以做到这一点。(然后,“堆栈帧”的概念实际上并不适用于STG。)

1

curinging关键(甚至是确定性地)取决于返回函数的能力。

考虑这个(伪造的)伪代码。

var f =(m,x,b)=> ...返回某物...

让我们规定调用少于三个参数的f返回一个函数。

var g = f(0,1); //这将返回绑定到0和1(m和x)的函数,该函数接受另一个参数(b)。

var y = g(42); //用缺少的第三个参数调用g,对m和x使用0和1

可以部分应用参数并返回一个可重用的函数(绑定到您确实提供的参数)非常有用(和DRY)。

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.