Haskell函数定义惯例

6

我是Haskell的初学者。

根据我的学习材料,函数定义中使用的约定实际上如下:

函数名称 参数由空格分隔 = 执行的代码

例:

f a b c = a * b +c

作为一名数学学生,我习惯使用以下类似的函数:

函数名称(由逗号分隔的参数)= 执行代码

例如:

f(a,b,c) = a * b + c

它在Haskell中运行。

我的疑问是它是否在所有情况下都有效?

我的意思是,我是否可以在Haskell函数定义中使用传统数学约定?

如果不对,在哪些特定情况下会出错?

提前致谢:)

3个回答

13

假设你想定义一个函数来计算直角三角形斜边的平方。以下两个定义都是有效的。

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

如果你确实使用了括号,那么就会出现错误!

首先需要注意的是函数的使用方式必须与定义方式相匹配。如果你用括号定义了函数,那么每次调用它都必须使用括号。如果你在定义函数时没有使用括号,那么调用它时就不能使用括号。

因此似乎没有理由喜欢其中一种写法而不喜欢另一种,只是个人口味问题。但实际上,我认为有一个很好的理由更喜欢没有括号的写法。有三个理由:

  1. 如果页面中没有括号干扰,代码看起来更加整洁,易于阅读。

  2. 如果到处都使用括号,性能将受到影响,因为每次使用函数都需要构造和解构一对括号(虽然编译器可能会优化掉这些步骤——我不确定)。

  3. 你想获得柯里化(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] 的同义词,它们的意思是相同的。


在你的第一组笔记中,“不完全相同”的意思暗示了它们之间有某种关系,但实际上并没有。柯里化只是允许简单的部分应用,但原则上它们之间是相互独立的。 - Xeo
2
@Xeo 它们有一定的关联。柯里化/元组形式形成一个伴随,其中F是(- x B),G是-^B - daniel gratzer
@Xeo 我想这是一个品味的问题。对于一个函数 (a,b,c) -> d,柯里化它会得到 a -> b -> c -> d,而部分应用第一个参数则会得到 a -> (b,c) -> d。对于两个参数的函数,柯里化和部分应用是相同的。在 Haskell 中,由于默认情况下进行柯里化,几乎没有区别(这就是我希望通过脚注避免发表评论的那种情况!) - Chris Taylor
@Chris:部分应用程序不会产生 a -> (b, c) -> d,但是正如其名称所示,它会应用函数的一部分,从而产生 (b, c) -> d - Xeo
@Xeo 当然,我包含了你应用的参数类型而你没有。我不认为这真的很重要。这就是为什么在同一句话中提到部分应用和柯里化几乎总是一个坏主意。 - Chris Taylor

2
  1. 数学符号不一致。如果所有函数都使用(,)作为参数,你必须写(+)((*)(a,b),c)来传递a*bc给函数+——当然,a*b是通过将ab传递给函数*来计算的。

  2. 可以用元组形式编写所有内容,但更难定义组合。虽然现在您可以指定类型a->b以涵盖任意数量的函数(因此,您可以将组合定义为一个类型为(b->c)->(a->b)->(a->c)的函数),但使用元组定义任意数量的函数要困难得多(现在a->b只会表示单参数函数;您无法再将众多参数的函数与众多参数的函数组合)。所以,在技术上是可能的,但需要一种语言特性使其变得简单和方便。


2
f(a,b,c) = a * b + c

需要理解的关键区别是,上述函数接受三元组并给出结果。实际上,您正在对三元组进行模式匹配。上述函数的类型类似于:

(a, a, a) -> a

如果你写出这样的函数:
f a b c = a * b + c

函数中自动进行柯里化。

您可以像这样编写代码 let b = f 3 2,它会通过类型检查,但使用初始版本则无法实现相同的效果。此外,像柯里化这样的技术可以帮助您在使用 (.) 组合各种函数时大有裨益,而前一种风格则无法实现,除非您正在组合三元组。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接