作为一个简单的示例,我使用了一个将两个值相加的函数(用ML)。没有咖喱的版本将是
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被列为功能语言的重要功能之一,但现在我对此有点不知所措。创建只使用每个参数的函数链而不是使用元组的函数的概念对于语法的简单更改而言似乎相当复杂。激励的动机,还是我错过了一些其他的优势,这些优势在我的简单示例中并不明显?只是语法糖吗?
#1 楼
使用咖喱函数,您可以更加专心地重用更多抽象函数。假设您有一个加法函数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
这里的语法比必须创建函数时要轻巧。 >
,或者如果您必须创建一个匿名lambda函数:假设您有两个查找功能。一个来自键/值对列表,一个键指向值,另一个来自映射图,键与值对以及键到值,如:
>然后,您可以创建一个接受从键到值的查找功能的功能。您可以将上面的任何查找函数传递给它,分别与列表或地图部分应用:
您可以使用轻量级语法专门化/部分应用函数,然后将这些部分应用的函数传递给更高阶的函数,例如
add2
或map
。高阶函数(将函数作为参数或将其作为结果)是函数式编程的基础,而currying和部分应用函数使高阶函数的使用更加有效和简洁。评论
值得注意的是,因此,Haskell中用于函数的参数顺序通常基于部分应用的可能性,这又使上述好处在更多情况下适用(ha,ha)。因此,默认情况下进行咖喱比从此处的具体示例所看到的更为有益。
– C. A. McCann
13年2月1日在21:08
at “一个来自键/值对列表,一个键指向一个值,另一个来自映射图,从键到值以及一个键到值”
–玛蒂·乌尔哈克(Mateen Ulhaq)
17年5月6日在7:46
@MateenUlhaq这是前一句话的延续,我想我们想基于一个键来获得一个值,并且我们有两种方法可以做到这一点。该句子列举了这两种方式。第一种方法是为您提供键/值对的列表以及我们要为其查找值的键,第二种方法是为我们提供适当的映射,然后再提供一个键。紧接句子后面的代码可能会有所帮助。
–鲍里斯(Boris)
17年5月8日在13:30
#2 楼
实际的答案是,使用currying使创建匿名函数更加容易。即使使用最小的lambda语法,这也是一个胜利。比较:map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]
如果您使用的lambda语法很丑,那就更糟了。 (我在看JavaScript,Scheme和Python。)
随着您使用越来越多的高阶函数,它变得越来越有用。尽管在Haskell中使用的高阶函数比其他语言使用的高阶函数,但实际上发现我实际上较少使用lambda语法,因为大约三分之二的时间,lambda只是部分应用的函数。 (还有很多其他时间,我将其提取到命名函数中。)
从根本上来说,哪个版本的函数是“规范的”并不总是很明显。例如,取
map
。 map
的类型可以用两种方式编写:map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])
哪个是“正确”的?实际上很难说。实际上,大多数语言都使用第一种语言-映射接受一个函数和一个列表,然后返回一个列表。但是,从根本上讲,map实际上所做的是将正常函数映射到列表函数-它接受一个函数并返回一个函数。如果map是咖喱的,那么您不必回答这个问题:它以一种非常优雅的方式完成了这两个问题。 >
此外,计算确实不是很复杂。实际上,与大多数语言使用的模型相比,它有点简化:您不需要将任何包含在您的语言中的多个参数的函数的概念表示出来。这也更紧密地反映了底层的lambda演算。
当然,ML风格的语言没有咖喱或非咖喱形式的多个参数的概念。
map
语法实际上对应于将元组f(a, b, c)
传入(a, b, c)
,因此f
仍然仅接受参数。实际上,这是我希望其他语言具有的非常有用的区别,因为它使编写类似以下内容变得很自然:具有多个参数思想的语言就诞生了!
评论
“ ML风格的语言没有咖喱或非咖喱形式的多个参数的概念”:就此而言,Haskell ML风格是吗?
–乔治
13年2月2日,14:35
@乔治:是的。
– Tikhon Jelvis
13年2月2日在17:47
有趣。我认识一些Haskell,并且我现在正在学习SML,因此有趣的是,看到了这两种语言之间的异同。
–乔治
13年2月2日在18:02
很好的答案,如果您仍然不相信,那就考虑一下类似于lambda流的Unix管道。
– Sridhar Sarnobat
16-10-14在23:23
“实用”的答案无关紧要,因为冗长的用法通常是通过部分应用而不是逐句地避免来避免。我在这里认为,lambda抽象的语法(尽管有类型声明)比Scheme中的(至少)丑陋,因为它需要更多内置的特殊语法规则来正确解析它,这使语言规范spec肿而没有任何收获关于语义属性。
– FrankHB
18年8月21日在16:34
#3 楼
如果您有一个传递为第一类对象的函数,并且没有在代码中的某个地方收到评估它所需的所有参数,则Currying可能很有用。您可以在获得一个或多个参数时简单地应用它们,然后将结果传递给具有更多参数的另一段代码,并在那里完成评估。实现此目的的代码比需要首先将所有参数集中在一起的代码要简单。
另外,还可以重复使用更多代码,因为带有单个参数的函数(另一个咖喱函数)不必与所有参数完全匹配。
#4 楼
(至少在最初时)进行主干的主要动机不是实践而是理论。特别是,使用currying可以有效地获取多参数函数,而无需实际为其定义语义或为产品定义语义。这导致一种更简单的语言具有与另一种更复杂的语言一样多的表现力,因此是所希望的。评论
虽然这里的动机是理论上的,但我认为简单性几乎总是一个实践优势。不用担心多参数函数会使我的编程工作变得更轻松,就像使用语义一样。
– Tikhon Jelvis
13年2月3日在20:41
@TikhonJelvis但是,在进行编程时,currying还使您担心其他事情,例如编译器无法捕捉到您向函数传递的参数太少的事实,或者在这种情况下甚至得到了错误的错误消息。当您不使用currying时,错误会更加明显。
– Alex R
13年2月4日在4:29
我从来没有遇到过这样的问题:GHC至少在这方面非常出色。编译器始终会捕获此类问题,并且对于该错误也具有良好的错误消息。
– Tikhon Jelvis
13年2月4日在6:35
我不同意错误消息的质量。可以维护,是的,但是还不够好。它也只会在导致类型错误的情况下捕获此类问题,即,如果您以后尝试将结果用作函数以外的其他功能(或者您已对类型进行注释,但是依靠它来读取可读错误有其自身的问题) );错误的报告位置与实际位置相离。
– Alex R
13年2月4日在14:45
#5 楼
(我将在Haskell中给出示例。)使用函数式语言时,可以部分应用函数非常方便。就像在Haskell的
(== x)
中一样,如果它的参数等于给定项True
,则返回x
的函数: mem :: Eq a => a -> [a] -> Bool
mem x lst = any (== x) lst
这与默认编程有关(另请参见Haskell Wiki上的Pointfree样式)。这种样式不着重于变量表示的值,而是着重于组成函数以及信息如何通过函数链流动。我们可以将示例转换为完全不使用变量的形式:从
==
到a
。通过简单地组成它们,我们得到了结果。这一切都归功于currying。例如,假设我们要将一个列表分为两部分-小于10的元素和其余部分,然后将这两个列表连接起来。列表的拆分由a -> Bool
any
(在这里我们也使用curried a -> Bool
)完成。结果为[a] -> Bool
类型。不用将结果提取到第一部分和第二部分中并使用partition
对其进行组合,我们可以通过直接使用(< 10)
作为mem x lst = any (\y -> y == x) lst
实际上,
<
评估为([Int],[Int])
。 。这些语言虽然对实际使用没有用,但从理论上来说非常重要。这与功能语言的基本属性有关-功能是一流的对象。如我们所见,从
++
到++
的转换意味着后者函数的结果是(uncurry (++) . partition (< 10)) [4,12,11,1]
类型。换句话说,结果是一个函数。(Un)currying与笛卡尔封闭类别密切相关,这是查看键入的λ演算的一种分类方式。
评论
对于“可读性差得多的代码”位,难道不是mem x lst = any(\ y-> y == x)lst吗? (带有反斜杠)。
–stusmith
13年2月7日在10:02
是的,谢谢您指出这一点,我会予以纠正。
–石油
13年2月7日在10:08
#6 楼
我还没有看到的另一件事是,curring允许对Arity进行(有限的)抽象。考虑Haskell库中包含的这些函数
在每种情况下,类型变量
c
都可以是函数类型,以便这些函数在其参数的参数列表的某些前缀上起作用。无需大惊小怪,您要么需要特殊的语言功能来抽象函数arity,要么具有专门针对不同arities的这些函数的许多不同版本。#7 楼
咖喱不仅是语法糖!考虑
add1
(未咖喱的)和add2
(咖喱的)的类型签名:情况下,类型签名中的括号是可选的,但为清楚起见,我将其包括在内。) add1
是一个使用int
并返回另一个函数的函数,而另一个函数随后又使用int
并返回int
。让我们定义一个将第一个参数应用于第二个参数的函数(非咖喱函数):现在,我们可以更清楚地看到add2
和int
之间的区别。 int
被一个2元组调用: /> add1 : (int * int) -> int
add2 : int -> (int -> int)
编辑:currying的本质好处是您可以免费获得部分申请。假设您想要一个类型为
int
的函数(例如,将其添加到列表中的add1
),并将其参数加5。您可以编写add2
,也可以使用内联lambda进行等效操作,但是也可以更轻松地(尤其是在这种情况不那么琐碎的情况下)编写add1
!评论
我知道我的示例在幕后有很大的不同,但是结果似乎是一个简单的语法更改。
–疯狂科学家
13年2月1日于19:54
咖喱并不是一个很深的概念。它是关于简化基础模型(请参阅Lambda演算)或使用具有元组的语言进行的简化,实际上是关于部分应用程序的语法便利性。不要低估语法便利性的重要性。
– Peaker
13年2月2日,13:15
#8 楼
咖喱只是语法糖,但我认为您对糖的作用有些误解。以您的示例为例,fun add x y = x + y
实际上是
fun add x = fn y => x + y
的语法糖,也就是说,(加x)返回一个带参数y的函数,并将x加到y。这两个功能实际上是完全不同的。
如果要在列表中的所有数字上加2:
br />
另一方面,如果要对列表中的每个元组求和,则addTuple函数非常合适。
[3,4,5]
。在部分应用有用的地方,咖喱函数非常有用-例如地图,折叠,应用,过滤器。考虑一下此函数,该函数返回提供的列表中的最大正数;如果没有正数,则返回0:
fun addTuple (x, y) = x + y
评论
我确实了解到curried函数具有不同的类型签名,并且实际上它是一个返回另一个函数的函数。我虽然缺少部分应用程序部分。
–疯狂科学家
13年2月2日在15:01
#9 楼
我有限的理解是这样的:1)局部函数应用程序
局部函数应用程序是返回带有较少参数的函数的过程。如果您提供3个参数中的2个,它将返回一个接受3-2 = 1个参数的函数。如果您提供3个参数中的1个,它将返回一个带有3-1 = 2个参数的函数。如果需要,甚至可以部分应用3个参数中的3个,它将返回不带参数的函数。
因此,请使用以下函数: />
将1绑定到x并将其部分应用于上述函数
f(x,y,z)
时,您将得到:f(x,y,z) = x + y + z;
其中:
f'(y,z) = 1 + y + z;
现在,如果将y绑定到2并将z绑定到3,然后部分应用
f'(y,z)
,您将得到: > 现在,您可以随时选择评估
f''() = 1 + 2 + 3
,f
或f'
。所以我可以做: >另一方面,咖喱是将一个函数拆分为一个包含一个参数函数的嵌套链的过程。您永远不能提供超过1个参数,它是1或0。因此,给定相同的功能:f(1,y,z) = f'(y,z);
如果您使用它,您将获得3个功能链:使用
f''
时:f'(2,3) = f''();
您将返回一个新函数: :
print(f''()) // and it would return 6;
返回一个新函数:
print(f'(1,1)) // and it would return 3;
最后,如果您用
f'(x)
调用x = 1
: f(x,y,z) = x + y + z;
您将返回
g(y)
。3)关闭
最后,闭包是将功能和数据作为单个单元一起捕获的过程。函数闭包可以接受0到无数个参数,但是它也知道没有传递给它的数据。
再次使用相同的函数:
f'(x) -> f''(y) -> f'''(z)
您可以改写闭包:
f'(x) = x + f''(y);
f''(y) = y + f'''(z);
f'''(z) = z;
其中:在
y = 2
上关闭。这意味着h(z)
可以读取z = 3
内部的x值。因此,如果要使用
6
调用f'
: d得到一个闭包:f'(1) = 1 + f''(y);
现在如果您用
x
和f'
调用f
:会返回
f
结论
过程,部分应用程序和闭包都有些相似,因为它们将函数分解为更多部分。
过程分解将多个参数的函数转换为单个参数的嵌套函数,这些嵌套函数返回单个参数的函数。引用一个或更少参数的函数是没有意义的,因为它没有意义。
部分应用程序将多个参数的函数分解为较小参数的函数,现在将其缺少的参数替换为
闭包将一个函数分解为一个函数和一个数据集,其中未传入函数的变量可以在数据集内查找要求值时要绑定的值。
所有这些令人困惑的是,它们可以被用来实现其他子集的一种。因此,从本质上讲,它们都是实现细节。它们都提供了相似的值,因为您不需要预先收集所有值,并且可以重用部分函数,因为您已经将其分解为离散的单元。
披露
我绝不是该主题的专家,我只是最近才开始学习这些知识,因此,我提供了我目前的理解,但是可能有错误,请您指出。我发现了。
评论
因此答案是:欺骗没有优势?
–天花板
16年12月13日在16:30
@ceving据我所知,这是正确的。在实践中,currying和部分应用将为您带来相同的好处。选择哪种语言来实现是出于实现的原因,一个给定的语言可能比另一个容易实现。
– DiidierA。
16年12月13日在17:20
#10 楼
通过Currying(部分应用程序),您可以通过固定一些参数从现有功能中创建新功能。这是词汇闭包的一种特殊情况,其中匿名函数只是一个琐碎的包装器,它将一些捕获的参数传递给另一个函数。我们也可以通过使用通用语法来进行词法闭包来做到这一点,但是部分应用程序提供了简化的语法糖。这就是为什么Lisp程序员在以函数样式工作时有时会使用库来部分化应用。代替
(lambda (x) (+ 3 x))
,它给我们的函数添加了3个参数,您可以编写类似(op + 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的函数没有参数(复数),它只有一个参数c。调用时,它将返回a-> b类型的函数。
– Max Heiber
17年5月26日在3:48
#11 楼
对于函数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是将一个将需要两个协议的函数转换为只需要一个返回结果的函数。部分评估
为什么要这样做?
在RHS上说
x
不仅是常规的int,而是复杂的计算需要花一会儿时间才能完成(为了增加效果),需要两秒钟。 > add : int * int -> int
类型的现在我们要为一系列数字计算此函数,让我们对其进行映射
x = twoSecondsComputation(z)
对于以上
twoSecondsComputation
的结果每一次都进行评估。这意味着需要6秒钟的计算时间。结合使用分阶段操作和循环操作可以避免这种情况。
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
只需要评估一次。要扩大规模,请用15分钟(或任何小时)替换两秒,然后对100个数字进行映射。部分评估。其目的无法真正证明。#12 楼
咖喱允许灵活的函数组成。我组成了一个函数“ 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变量是一个函数,该函数返回一个函数,该函数返回一个接受输入的函数。这是一个简单有用的示例,而不是可见的对象。
#13 楼
当您没有函数的所有参数时,使用Currying是一个优点。如果您恰好完全评估了该功能,则没有什么明显的区别。通过固化,您可以避免提及尚不需要的参数。它更加简洁,不需要查找不会与范围内的另一个变量发生冲突的参数名称(这是我最喜欢的好处)。例如,在使用将函数用作参数时,通常会遇到需要“将3加到输入”或“将输入与变量v比较”之类的函数的情况。借助curring,可以轻松编写以下函数:
add 3
和(== v)
。您必须使用lambda表达式,而无需进行粗略的表达:x => add 3 x
和x => x == v
。 lambda表达式的长度是原来的两倍,并且如果在范围上已经存在x
,那么除了x
之外,还有少量的忙于挑选名字。在为函数编写通用代码时,基于参数的数量,最终不会有数百种变体。例如,在C#中,“ curry”方法将需要Func #14 楼
我能想到的主要推理(无论如何我都不是这个主题的专家)开始显示其功能从微不足道变为非微不足道的好处。在具有这种性质的大多数概念的所有琐碎情况下,您都不会发现真正的好处。但是,大多数功能语言在处理操作中大量使用堆栈。考虑以PostScript或Lisp为例。通过使用curring,可以更有效地堆叠功能,并且随着操作的增长越来越小,这种好处变得显而易见。以咖喱的方式,命令和参数可以按顺序扔到堆栈上,并根据需要弹出,以便它们以正确的顺序运行。评论
究竟需要多少堆栈框架才能使工作效率更高?
–梅森·惠勒
13年2月1日在19:55
@MasonWheeler:我不知道,因为我说我不是函数式语言专家或专门使用Curry的专家。因此,我标记了这个社区Wiki。
–乔尔·埃瑟顿(Joel Etherton)
13年2月1日在19:58
@MasonWheeler您有一个要点这个答案的措词,但让我插话说,实际创建的堆栈帧数量在很大程度上取决于实现。例如,在无脊椎无标签G机(STG; GHC实现Haskell的方式)中,延迟实际评估,直到它累积了所有(或至少需要知道的尽可能多的)参数为止。我似乎不记得是对所有函数还是仅对构造函数执行此操作,但是我认为大多数函数应该都可以做到。 (然后,“堆栈框架”的概念实际上并不适用于STG。)
–user7043
13年2月1日在20:09
#15 楼
固化至关重要(甚至是绝对)取决于返回函数的能力。考虑此(伪造)伪代码。
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)。
评论
单独进行咖喱基本上是没有用的,但是默认情况下让所有函数咖喱都会使许多其他功能更易于使用。在您实际上已经使用功能性语言一段时间之前,很难欣赏到它。delnan在评论JoelEtherton的答案时提到的一件事,但我想我要明确提到的是(至少在Haskell中),您不仅可以部分地应用函数,而且可以使用类型构造函数-这可以是相当不错的。便利;这可能是需要考虑的事情。
所有人都给出了Haskell的示例。有人可能会想到,currying仅在Haskell中有用。
@ManojR在Haskell中都没有给出示例。
这个问题引起了有关Reddit的相当有趣的讨论。