我认为我不太了解柯里化,因为我看不到它可以提供任何巨大的好处。也许有人可以用一个例子来启发我,说明它为什么如此有用。它真的有好处和应用,还是只是一个被过度欣赏的概念?
7 回答
( currying和partial application之间存在细微差别,尽管它们密切相关;由于它们经常混合在一起,我将同时处理这两个术语。)
我第一次意识到好处的地方是当我看到切片运算符时:
incElems = map (+1)
--non-curried equivalent: incElems = (\elems -> map (\i -> (+) 1 i) elems)
IMO,这很容易阅读。现在,如果 的类型(+)
是(Int,Int) -> Int
*,它是未curried 的版本,它会(反直觉地)导致错误——但是 curryied,它按预期工作,并且具有 type [Int] -> [Int]
。
您在评论中提到了 C# lambdas。在 C# 中,你可以这样写incElems
,给定一个函数plus
:
var incElems = xs => xs.Select(x => plus(1,x))
如果您习惯于无点样式,您会发现x
这里是多余的。从逻辑上讲,该代码可以简化为
var incElems = xs => xs.Select(curry(plus)(1))
由于缺少 C# lambdas 的自动部分应用程序,这很糟糕。这是决定柯里化在哪里真正有用的关键点:主要是当它隐式发生时。对我来说,map (+1)
是最容易阅读的,然后是,如果没有很好的理由,应该避免.Select(x => plus(1,x))
使用带有的版本。curry
现在,如果可读,好处总结为更短、更易读和更少混乱的代码——除非有一些滥用无点风格的做法(我很喜欢(.).(.)
,但它是……特别的)
此外,如果不使用 curried 函数,lambda 演算将变得不可能,因为它只有一值(但因此是高阶)函数。
* 当然它实际上是在 中Num
,但目前它更具可读性。
更新:currying 是如何工作的。
查看plus
C# 中的类型:
int plus(int a, int b) {..}
你必须给它一个值的元组——不是用 C# 术语,而是用数学语言;你不能只忽略第二个值。用haskell的话来说,就是
plus :: (Int,Int) -> Int,
可以像这样使用
incElem = map (\x -> plus (1, x)) -- equal to .Select (x => plus (1, x))
输入的字符太多了。假设您希望将来更频繁地执行此操作。这里有一个小帮手:
curry f = \x -> (\y -> f (x,y))
plus' = curry plus
这使
incElem = map (plus' 1)
让我们将其应用于具体值。
incElem [1]
= (map (plus' 1)) [1]
= [plus' 1 1]
= [(curry plus) 1 1]
= [(\x -> (\y -> plus (x,y))) 1 1]
= [plus (1,1)]
= [2]
在这里你可以curry
在工作中看到。它将标准的 haskell 风格的函数应用程序 ( plus' 1 1
) 转换为对“tupled”函数的调用——或者,从更高的层次来看,将“tupled”转换为“untupled”版本。
幸运的是,大多数时候,您不必担心这一点,因为有自动部分应用程序。
这不是自切片面包以来最好的事情,但如果你仍然使用 lambdas,那么在不使用 lambda 语法的情况下使用高阶函数会更容易。比较:
map (max 4) [0,6,9,3] --[4,6,9,4]
map (\i -> max 4 i) [0,6,9,3] --[4,6,9,4]
当你使用函数式编程时,这些类型的结构经常出现,这是一个很好的捷径,可以让你从更高的层次考虑问题——你映射的是“ max 4
”函数,而不是随机的恰好被定义为的函数(\i -> max 4 i)
。它使您可以更轻松地开始思考更高级别的间接性:
let numOr4 = map $ max 4
let numOr4' = (\xs -> map (\i -> max 4 i) xs)
numOr4 [0,6,9,3] --ends up being [4,6,9,4] either way;
--which do you think is easier to understand?
也就是说,它不是灵丹妙药。有时你的函数参数对于你试图用currying做的事情的顺序是错误的,所以无论如何你都必须求助于lambda。然而,一旦你习惯了这种风格,你就会开始学习如何设计你的功能来很好地配合它,一旦这些神经元开始在你的大脑中连接起来,相比之下,以前复杂的结构就会开始变得明显。
柯里化的一个好处是它允许部分应用函数而不需要任何特殊的语法/运算符。一个简单的例子:
mapLength = map length
mapLength ["ab", "cde", "f"]
>>> [2, 3, 1]
mapLength ["x", "yz", "www"]
>>> [1, 2, 3]
map :: (a -> b) -> [a] -> [b]
length :: [a] -> Int
mapLength :: [[a]] -> [Int]
由于currying,该map
函数可以被认为具有类型(a -> b) -> ([a] -> [b])
,因此当length
作为它的第一个参数应用时,它会产生mapLength
类型的函数[[a]] -> [Int]
。
柯里化具有其他答案中提到的便利功能,但它通常也有助于简化对语言的推理或实现一些代码比其他方式更容易。例如,柯里化意味着任何函数都具有与a ->b
. 如果您编写一些类型为 的a -> b
代码,则该代码可以与任何函数一起使用,无论它需要多少参数。
最著名的例子是这个Applicative
类:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
和一个例子使用:
-- All possible products of numbers taken from [1..5] and [1..10]
example = pure (*) <*> [1..5] <*> [1..10]
在这种情况下,pure
并<*>
调整任何类型的函数以a -> b
使用类型列表[a]
。由于部分应用,这意味着您还可以调整类型的函数a -> b -> c
以使用[a]
and [b]
,或a -> b -> c -> d
使用[a]
,[b]
and [c]
,等等。
之所以有效,是因为a -> b -> c
与以下内容相同a -> (b -> c)
:
(+) :: Num a => a -> a -> a
pure (+) :: (Applicative f, Num a) => f (a -> a -> a)
[1..5], [1..10] :: Num a => [a]
pure (+) <*> [1..5] :: Num a => [a -> a]
pure (+) <*> [1..5] <*> [1..10] :: Num a => [a]
柯里化的另一个不同用途是 Haskell 允许您部分应用类型构造函数。例如,如果您有这种类型:
data Foo a b = Foo a b
...在许多情况下编写实际上是有意义的Foo a
,例如:
instance Functor (Foo a) where
fmap f (Foo a b) = Foo a (f b)
即,Foo
是一个带有 kind 的双参数类型构造函数* -> * -> *
;Foo a
,仅对一种类型的部分应用Foo
,是一种类型构造函数 kind * -> *
。 Functor
是一个类型类,只能为 kind 的类型构造器实例化* -> *
。既然Foo a
是这种,你可以Functor
为它做一个实例。
在不指定您提出问题的上下文的情况下询问柯里化的好处是有些可疑的:
- 在某些情况下,如函数式语言,柯里化只会被视为具有更多本地变化的东西,您可以用显式的元组域替换东西。然而,这并不是说柯里化在这些语言中没有用。从某种意义上说,使用 curried 函数进行编程会让您“感觉”自己正在以更具功能性的风格进行编程,因为您通常会面临处理高阶函数的情况。当然,大多数时候,您将“填写”函数的所有参数,但在您希望以部分应用形式使用该函数的情况下,以柯里化形式执行此操作要简单一些。
curry
并且uncurry
也有助于函数式编程语言中的某些便利,我可以将 Haskell 中的箭头视为您将使用的特定示例以及curry
将uncurry
事物应用于箭头的不同部分等... - 在某些情况下,您想考虑的不仅仅是函数式程序,您可以将 currying / uncurrying 作为一种方式来说明构造逻辑的消除和引入规则,这提供了一个连接到一个更优雅的动机来说明它存在的原因。
- 在某些情况下,例如在 Coq 中,使用柯里化函数与元组函数可以产生不同的归纳方案,这可能更容易或更难使用,具体取决于您的应用程序。
I used to think that currying was simple syntax sugar that saves you a bit of typing. For example, instead of writing
(\ x -> x + 1)
I can merely write
(+1)
The latter is instantly more readable, and less typing to boot.
So if it's just a convenient short cut, why all the fuss?
Well, it turns out that because function types are curried, you can write code which is polymorphic in the number of arguments a function has.
For example, the QuickCheck
framework lets you test functions by feeding them randomly-generated test data. It works on any function who's input type can be auto-generated. But, because of currying, the authors were able to rig it so this works with any number of arguments. Were functions not curried, there would be a different testing function for each number of arguments - and that would just be tedious.