假设你想定义一个函数来计算直角三角形斜边的平方。以下两个定义都是有效的。
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
。如果不包含它们,将会出现错误(可能现在看起来很困惑,但放心,当您学习了类型类之后,这将会变得清晰明了)。
现在看看hyp1
,解析类型a -> a -> a
的一种方式是它接受两个类型为a
的元素并返回另一个类型为a
的元素。你可以这样使用:
>> hyp1 3 4
25
如果你确实使用了括号,那么就会出现错误!
首先需要注意的是函数的使用方式必须与定义方式相匹配。如果你用括号定义了函数,那么每次调用它都必须使用括号。如果你在定义函数时没有使用括号,那么调用它时就不能使用括号。
因此似乎没有理由喜欢其中一种写法而不喜欢另一种,只是个人口味问题。但实际上,我认为有一个很好的理由更喜欢没有括号的写法。有三个理由:
如果页面中没有括号干扰,代码看起来更加整洁,易于阅读。
如果到处都使用括号,性能将受到影响,因为每次使用函数都需要构造和解构一对括号(虽然编译器可能会优化掉这些步骤——我不确定)。
你想获得柯里化(Currying)的好处,也就是部分应用函数*。
最后一点有点微妙。回忆一下我之前所说的,可以把类型为 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!"]
相当有用!
我希望我已经说服了你,学校教育材料中使用的约定有一些非常坚实的理由。作为一个数学背景的人,我能够理解使用更传统的语法的吸引力,但我希望随着你在编程旅程中的进步,你能够看到改变成一些最初可能有点陌生的东西的优势。
* pedants 注意事项 - 我知道柯里化和部分应用不完全相同。
** 实际上 GHCI 会告诉你类型是 bang :: [[Char]] -> [[Char]]
但是因为 String
是 [Char]
的同义词,它们的意思是相同的。
(- x B)
,G是-^B
。 - daniel gratzer(a,b,c) -> d
,柯里化它会得到a -> b -> c -> d
,而部分应用第一个参数则会得到a -> (b,c) -> d
。对于两个参数的函数,柯里化和部分应用是相同的。在 Haskell 中,由于默认情况下进行柯里化,几乎没有区别(这就是我希望通过脚注避免发表评论的那种情况!) - Chris Taylora -> (b, c) -> d
,但是正如其名称所示,它会应用函数的一部分,从而产生(b, c) -> d
。 - Xeo