currying有什么好处?

时间:2012-09-13 19:32:31

标签: haskell functional-programming currying

我认为我不太了解currying,因为我无法看到它可以提供任何巨大的好处。也许有人可以用一个例子来启发我,说明它为何如此有用。它真的有好处和应用,还是仅仅是一个过度赞赏的概念?

7 个答案:

答案 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
  • 为此,我们构建了一个af的封闭(目前我们根本不评估f
  • 然后一段时间后,我们收到第二个参数b : B
  • 现在我们同时拥有AB参数,我们可以用原始格式评估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 请注意,这种状态传递通常可以通过StateST monad更好地完成,这在这个例子中并不是特别有用

2 是的,这是一个lambda符号。我希望我不会混淆任何人 - 幸运的是,在Haskell中,很明显 lambda函数是用\编写的,而不是λ

答案 5 :(得分:2)

在没有说明你提出这个问题的背景下,问一下curry的好处有点可疑:

  • 在某些情况下,与函数式语言一样,currying只会被视为具有更多本地更改的东西,您可以使用显式的tupled域替换。然而,这并不是说currying在这些语言中是无用的。从某种意义上说,使用curried函数进行编程会让你“感觉”像是在编写一个更实用的函数,因为你更常遇到处理更高阶函数的情况。当然,大多数情况下,您将“填写”函数的所有参数,但是在您希望以部分应用的形式使用该函数的情况下,以curry形式执行此操作会更简单一些。我们通常告诉我们的初级程序员在学习函数式语言时使用它只是因为它感觉更好的风格并提醒他们他们的编程不仅仅是C.使curryuncurry之类的东西也有帮助对于函数式编程语言中的某些便利,我可以将Haskell中的箭头视为您将curryuncurry用于将事物应用于箭头的不同部分等的具体示例。 ..
  • 在某些情况下,你想要考虑的不仅仅是功能性程序,你可以将currying / uncurrying作为一种方式来陈述消除和引入规则以及建设性逻辑,这提供了一种更优雅动机的联系,为什么它存在。
  • 在某些情况下,例如,在Coq中,使用curried函数与tupled函数可以产生不同的归纳方案,这可能更容易或更难处理,具体取决于您的应用程序。

答案 6 :(得分:1)

我曾经认为currying是简单的语法糖,可以节省一些打字。例如,而不是写

(\ x -> x + 1)

我只能写

(+1)

后者立即更具可读性,并且更少打字输入。

所以如果它只是一个方便的捷径,为什么所有的大惊小怪?

嗯,事实证明,因为函数类型是curry,你可以编写函数所具有的参数个数的多态代码。

例如,QuickCheck框架允许您通过提供随机生成的测试数据来测试函数。它适用于任何可以自动生成输入类型的函数。但是,由于讨论,作者能够装备它,所以这适用于任意数量的参数。如果函数不是,那么每个参数的数量都会有不同的测试函数 - 而这只会很乏味。