我认为我不太了解currying,因为我无法看到它可以提供任何巨大的好处。也许有人可以用一个例子来启发我,说明它为何如此有用。它真的有好处和应用,还是仅仅是一个过度赞赏的概念?
答案 0 :(得分:12)
( currying 和部分应用程序之间存在细微差别,虽然它们密切相关;因为它们经常混合在一起,我会处理两者条款。)
我首先意识到好处的地方是我看到切片操作员的时候:
incElems = map (+1)
--non-curried equivalent: incElems = (\elems -> map (\i -> (+) 1 i) elems)
IMO,这很容易阅读。现在,如果(+)
的类型是(Int,Int) -> Int
*,这是一个未经证实的版本,它会(反直觉地)导致错误 - 但是会发生错误,它会按预期工作,并且类型为{ {1}}。
你在评论中提到了C#lambdas。在C#中,您可以编写[Int] -> [Int]
这样的函数,给定函数incElems
:
plus
如果你习惯了无点型,你会发现这里的var incElems = xs => xs.Select(x => plus(1,x))
是多余的。从逻辑上讲,该代码可以简化为
x
由于缺少使用C#lambdas的自动部分应用程序而非常糟糕。这是确定currying实际有用的关键点:主要是当它发生隐式时。对我来说,var incElems = xs => xs.Select(curry(plus)(1))
是最容易阅读的,然后来map (+1)
,如果没有充分的理由,可能应该避免带.Select(x => plus(1,x))
的版本。
现在,如果可读,那么好处总结为更短,更易读,更简洁的代码 - 除非有一些滥用无点样式的做法(我很喜欢{{ 1}},但它是......特别的)
此外,如果不使用curried函数,lambda演算将变得不可能,因为它只有一个值(但因此更高阶)函数。
*当然它实际上在curry
中,但目前它的可读性更高。
更新:实际上如何干练。
查看C#中的(.).(.)
类型:
Num
你必须给它一个值的元组 - 不是用C#术语,而是用数学方式说的;你不能只留下第二个价值。在haskell术语中,那是
plus
可以像
一样使用int plus(int a, int b) {..}
输入的字符太多了。假设您希望将来更频繁地这样做。这是一个小帮手:
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)
的工作情况。它将标准的haskell样式函数应用程序(incElem [1]
= (map (plus' 1)) [1]
= [plus' 1 1]
= [(curry plus) 1 1]
= [(\x -> (\y -> plus (x,y))) 1 1]
= [plus (1,1)]
= [2]
)转换为对“tupled”函数的调用 - 或者,在更高级别查看,将“tupled”转换为“untupled”版本。
幸运的是,大多数情况下,您不必担心这一点,因为有自动部分应用。
答案 1 :(得分:7)
这不是自切片面包以来最好的东西,但如果你还在使用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]
当你使用函数式编程时,这些类型的结构经常出现,这是一个很好的快捷方式,让你从稍高的层面思考问题 - 你映射到“{{1 “函数,而不是一些恰好被定义为max 4
的随机函数。它可以让您更容易地开始更高层次的间接思考:
(\i -> max 4 i)
那就是说,它不是灵丹妙药;有时你的函数的参数对于你尝试用currying做的事情是错误的顺序,所以你无论如何都要求助于lambda。然而,一旦你习惯了这种风格,你就会开始学习如何设计你的功能以便与它一起工作,一旦这些神经元开始连接你的大脑,以前复杂的构造就会开始显得比较明显。
答案 2 :(得分:4)
currying的一个好处是它允许部分应用函数而无需任何特殊的语法/运算符。一个简单的例子:
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]
。
答案 3 :(得分:3)
Currying具有其他答案中提到的便利功能,但它通常也用于简化语言推理或实现某些代码,这比其他方式更容易。例如,currying意味着任何函数都具有与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]
和[b]
,或a -> b -> c -> d
使用[a]
,{{1 }和[b]
等等。
这可行的原因是因为[c]
与a -> b -> c
相同:
a -> (b -> c)
另一种不同的currying用法是Haskell允许你部分应用类型构造函数。例如,如果你有这种类型:
(+) :: 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]
......在许多情境中编写data Foo a b = Foo a b
实际上是有意义的,例如:
Foo a
即,instance Functor (Foo a) where
fmap f (Foo a b) = Foo a (f b)
是具有种类Foo
的双参数类型构造函数; * -> * -> *
,Foo a
仅部分应用于一种类型,是一种类型Foo
的类型构造函数。 * -> *
是一个类型类,只能为类型Functor
的类型构造函数实例化。由于* -> *
属于此类,因此您可以为其创建Foo a
个实例。
答案 4 :(得分:3)
部分应用的“无干扰”形式如下:
f : (A ✕ B) → C
a : A
a
和f
的封闭(目前我们根本不评估f
)b : B
A
和B
参数,我们可以用原始格式评估f
... a
,并评估f(a,b)
。有点复杂,不是吗?
当f
首先被咖喱时,它更简单:
f : A → B → C
a : A
- 我们可以做:f a
b : B
f a
应用于b
。到目前为止,这很好,但比简单更重要,这也为我们提供了实现函数的额外可能性:我们可以在收到a
参数后立即进行一些计算,并进行这些计算即使用多个不同的b
参数评估函数,也不需要在以后完成!
举一个例子,考虑this audio filter,无限脉冲响应过滤器。它的工作方式如下:对于每个音频样本,您使用一些状态参数(在这种情况下,一个简单的数字,开头为0)和音频样本提供“累加器函数”(f
)。该函数然后做了一些魔术,然后吐出 new 内部状态 1 和输出样本。
现在这是至关重要的一点 - 这个函数的作用取决于系数 2 λ
,这不是一个常数:它取决于我们的截止频率d喜欢过滤器(这将控制“滤波器将如何发声”)以及我们正在处理的采样率。不幸的是,λ
的计算有点复杂(lp1stCoeff $ 2*pi * (νᵥ ~*% δs)
其余的魔法,所以我们不想再为每一个样本做这件事了。非常烦人,因为νᵥ
和δs
几乎不变:他们很少改变,当然不是每个音频样本。
但是currying节省了一天!只要我们有必要的参数,我们就会立即计算λ
。然后,在许多音频样本中的每一个中,我们只需要执行剩下的,非常简单的魔法:yⱼ = yⱼ₁ + λ ⋅ (xⱼ - yⱼ₁)
。所以我们是高效的,并且仍然保持一个很好的安全参考透明的纯功能界面。
1 请注意,这种状态传递通常可以通过State
或ST
monad更好地完成,这在这个例子中并不是特别有用
2 是的,这是一个lambda符号。我希望我不会混淆任何人 - 幸运的是,在Haskell中,很明显 lambda函数是用\
编写的,而不是λ
。
答案 5 :(得分:2)
在没有说明你提出这个问题的背景下,问一下curry的好处有点可疑:
curry
和uncurry
之类的东西也有帮助对于函数式编程语言中的某些便利,我可以将Haskell中的箭头视为您将curry
和uncurry
用于将事物应用于箭头的不同部分等的具体示例。 .. 答案 6 :(得分:1)
我曾经认为currying是简单的语法糖,可以节省一些打字。例如,而不是写
(\ x -> x + 1)
我只能写
(+1)
后者立即更具可读性,并且更少打字输入。
所以如果它只是一个方便的捷径,为什么所有的大惊小怪?
嗯,事实证明,因为函数类型是curry,你可以编写函数所具有的参数个数的多态代码。
例如,QuickCheck
框架允许您通过提供随机生成的测试数据来测试函数。它适用于任何可以自动生成输入类型的函数。但是,由于讨论,作者能够装备它,所以这适用于任意数量的参数。如果函数不是,那么每个参数的数量都会有不同的测试函数 - 而这只会很乏味。