假设你想定义一个函数来计算右三角形的hypoteneuse的平方。以下定义中的任何一个都是有效的
hyp1 a b = a * a + b * b
hyp2(a,b) = a * a + b * b
但是,它们不是相同的功能!您可以通过在GHCI
看着自己的类型
>> :type hyp1
hyp1 :: Num a => a -> a -> a
>> :type hyp2
hyp2 :: Num a => (a, a) -> a
以hyp2
第一(现在忽略Num a =>
一部分)的类型告诉你的函数取一对(a, a)
并返回另一个a
(例如,它可能需要告诉一对整数并返回另一个整数或一对实数并返回另一个实数)。你这样使用它
>> hyp2 (3,4)
25
请注意,括号在这里不是可选的!他们确保论证是正确的类型,一对a
s。如果你不包含它们,你会得到一个错误(现在看起来可能会让你感到困惑,但请放心,当你了解了类型类时它会有意义的)。
现在看hyp1
读取类型a -> a -> a
的一种方法是需要两件东西a
并返回a
类型的其他东西。您可以使用它像这样
>> hyp1 3 4
25
现在你会得到一个错误,如果你做包括括号!
所以首先要注意的是,你使用函数的方式必须与你定义它的方式相匹配。如果用parens定义函数,则每次调用它时都必须使用parens。如果在定义函数时不使用parens,则在调用它时不能使用它们。
因此,似乎没有理由选择其中一个 - 这只是一个品味问题。但实际上,我认为是是一个优先于另一个的好理由,并且您应该更喜欢没有括号的样式。有三个很好的理由:
它看起来更干净,让您的代码更容易阅读,如果你没有parens凌乱的页面。
如果你在任何地方都使用parens,那么你会受到性能影响,因为每次使用该函数时都需要构造和解构一对(尽管编译器可能会优化它 - 我不确定)。
你想要得到的好处讨好,又名部分应用功能 *。
最后一点是微妙的。回想一下,我说过一种理解a -> a -> a
类型函数的方法是它需要两件事a
类型,并返回另一个a
。但是还有另一种方法来读取该类型,即a -> (a -> a)
。这意味着完全一样的东西,因为->
运算符在Haskell中是右关联的。解释是该函数只需要一个a
,并返回a -> a
类型的函数。这可以让你只需要提供的第一个参数的功能,并应用第二个参数后,例如
>> let f = hyp1 3
>> f 4
25
这是在各种各样的场合实用价值。例如,map
功能可让您将一些功能列表中的每个元素 -
>> :type map
map :: (a -> b) -> [a] -> [b]
假设你有增加了一个爆炸任何String
功能(++ "!")
。但是你有Strings
的列表,你希望它们都以爆炸声结束。没问题!你只是部分应用map
功能
>> let bang = map (++ "!")
现在bang
是类型的函数**
>> :type bang
bang :: [String] -> [String]
,你可以用它像这样
>> bang ["Ready", "Set", "Go"]
["Ready!", "Set!", "Go!"]
非常有用的!
我希望我已经说服你,在你的学校的教育材料中使用的惯例有一些相当坚实的理由被使用。作为拥有数学背景的人,我可以看到使用更多“传统”语法的吸引力,但我希望随着您在编程之旅中的进步,您将能够看到改变最初的某些东西的优势对你不熟悉。
*对于书呆子的说明 - 我知道咖喱和部分应用并不完全一样。
**实际上,GHCI会告诉您类型为bang :: [[Char]] -> [[Char]]
,但由于String
是[Char]
的同义词,所以这些意思是相同的。
在你的第一个笔记上:'不完全相同的东西'暗示着某种关系,而实际上没有关系。 Currying只允许轻松部分应用,但我认为原则上它们是非常不相关的。 – Xeo
@Xeo他们有些相关。 curried/tupled表单形成一个[adjunction](http://en.wikipedia.org/wiki/Adjoint_functors),其中F是'( - x B)',G是' -^B'。 – jozefg
@Xeo我想这是一个味道点。对于一个函数'(a,b,c) - > d',currying它给出'a - > b - > c - > d'而部分应用第一个参数给出a - >(b,c) - > D'。对于两个参数的函数,currying和部分应用是相同的事情。在Haskell中,几乎没有区别,因为默认情况下是currying(这是我希望避免用我的脚注做出的评论!) –