跳转到内容

Haskell/Preliminaries

来自维基教科书,开放世界中的开放书籍

注意

此页面不是 Haskell 维基教科书的主要部分。它与主要介绍性材料有重叠,但没有那么完善。它对命令式编程背景有一些假设。因为学习在从多个角度体验事物时效果最好,所以将此作为补充材料来强化基本概念,可能比简单地重新阅读其他章节更有效。不过,此页面基本上是多余的。


(本节中的所有示例都可以键入到 Haskell 源文件中,并通过将该文件加载到 GHC 或 Hugs 中来进行评估。)

不改变的变量和改变的函数

[编辑 | 编辑源代码]

函数式编程背后的主要思想之一是引用透明性——即函数和参数始终可以被函数的返回值替换。例如,表达式2+2可以与值4互换。这有利于 Haskell 中的一些重要概念,包括惰性求值和能够重新排序表达式以并发执行它们的能力。

与 C++ 等命令式语言不同,Haskell 不将变量视为内存位置。在 Haskell 中,变量绑定到其存在的任何范围内特定值。在大多数情况下,它们更像是常量而不是变量。变量在数学中的概念实际上与在 Haskell 中相同。在以下示例中,x 绑定到 3;它不可能改变。

x = 6 / 2

如果你习惯了命令式编程,这可能看起来非常奇怪。如何在没有实际变量的情况下编写有用的程序?好吧,Haskell 的函数参数可以起到相同的目的:一旦你将值传递给函数,该函数就可以在任何地方使用它。要更改参数的值,只需再次调用该函数即可。所以答案是递归。

以下是一些 Haskell 声明

x = 3
y = x * 2

你可能已经猜到,最后 y = 6。看起来很像数学,对吧?

但是,与传统语言不同,顺序无关紧要。Haskell 没有“控制流”的概念,因此它不会从顶部开始执行并一直执行到最后。所以这意味着与上面的完全相同

y = x * 2
x = 3

你当然也可以定义函数。这就是生活开始变得有趣的地方。

double x = x * 2
z = double y

这表示函数“double”接受一个参数“x”,结果等于 x 的两倍。注意缺少括号:在 Haskell 中,“double y”表示将“double”函数应用于值“y”。括号只用于确定求值优先级,就像数学中一样。

到目前为止,这让你能够完成电子表格所能做的事情:这个世界中的变量看起来非常像电子表格中的单元格,你可以设置函数来“处理”这些变量。

现在看看这个函数

factorial 0 = 1
factorial n = n * factorial (n-1)

这定义了一个名为“factorial”的新函数。第一行表示 0 的阶乘为 1,第二行表示任何其他数字“n”的阶乘等于“n”乘以“n-1”的阶乘。注意“n-1”周围的括号:如果没有这些括号,它将被解析为“(factorial n) - 1”;函数应用(如这个过程所知)具有比加法或求幂等任何正则运算符更高的优先级。

这显示了如何在 Haskell 中进行“循环”。你没有像“n = n - 1”这样的循环计数器,因为这会改变“n”的值。因此,你使用递归。

与我们之前的示例不同,两个递归声明的顺序很重要。Haskell 从顶部开始匹配函数调用,并选择第一个匹配的函数。因此,如果两个声明颠倒,则编译器将得出结论,阶乘 0 等于 0 * 阶乘 -1,依此类推,直到无穷大。这不是我们想要的。

尾递归

[编辑 | 编辑源代码]

如果你想知道堆栈空间,所有这些递归最终会不会导致堆栈溢出?答案是 Haskell 支持尾递归(每个体面的函数式语言的编译器都支持)。尾递归的思想是,如果函数最后做的事情是调用自身,那么解释器不妨不必将东西放到堆栈中,而只需使用函数的新参数“跳转”到开头。不过,在这种情况下,阶乘函数必须用“(n-1)”调用自身,然后将结果乘以“n”。

因此,我们的阶乘函数不是尾递归的(因为它必须在调用自身后将结果乘以 n)。但是,这个函数是尾递归的

factorial n = factorial' n 1

factorial' 0 a = a
factorial' n a = factorial' (n-1) (n*a)

这里有两个函数。它们的名字几乎相同,除了第二个函数有一个撇号。(Haskell 标识符允许包含撇号。)按照惯例,撇号用于表示某事物的变体。

这里,factorial'函数与以前一样,接受一个参数n,但它还接受一个第二个“累加器”参数。由于调用自身是它做的最后一件事,因此尾递归规则适用:不会将任何东西放到堆栈中;factorial'函数只是像它的参数始终是 n-1 和 n*a 一样执行。

(注意这个参数与变量的相似性。参数在某种程度上是 Haskell 中的变量。)

使用此函数的程序员不想担心累加器参数,因此使用普通阶乘函数来“封装”factorial',方法是提供累加器的初始值。

将阶乘函数键入到 Haskell 源文件中,并将其加载到您喜欢的 Haskell 环境中。

5 的阶乘是多少?

1000 的阶乘呢?这是你预期的结果吗?

-1 的阶乘呢?

你可能已经注意到上面有一些有趣的事情。你的参数或任何东西都没有声明为任何特定类型。

这是因为 Haskell 使用类型系统来推断每个表达式的类型,即 Hindley-Milner 类型系统。但 Haskell 不是弱类型;它与 C++ 一样是强类型,因此你不会遇到运行时类型错误。任何编译的程序都保证不会出现类型错误。另一个好处是,当你犯错误时,它几乎总会改变结果表达式的类型,并且编译器会将其标记为错误。(你的程序 simply won't compile。)你可能仍然希望出于一些原因为你的函数编写类型。

  1. 它有助于记录你的程序。
  2. 当你犯错时,类型声明有助于缩小问题的根源。
  3. 最重要的是,它将帮助你学习。理解类型系统是掌握 Haskell 的基础。

类型声明的语法如下

x :: Integer
factorial :: Integer -> Integer

你应该将第一个声明理解为“x 的类型为 Integer”或“x 具有类型 Integer”。'::' 告诉我们我们指的是一个类型。你会注意到阶乘看起来有点不同。它是一个接受 Integer 并返回 Integer 的函数。稍后你会明白为什么我们在描述类型时使用 ->,以及为什么描述函数类型在语法上与描述变量类型如此接近。

最后一点:以大写字母开头的标识符保留用于类型、模块和数据构造函数(有点像命名空间,现在不用担心)。如果你尝试声明一个以大写字母开头的变量或函数,编译器会报错。

华夏公益教科书