跳转至内容

Haskell/入门

来自 Wikibooks,开放世界的开放书籍

注意

此页面不是 Haskell Wikibook 的主要部分。它与主要的入门材料有重叠。因为从多个角度体验事物是学习的最佳方式,所以阅读本文作为补充以加强基本概念可能会有用。

此页面在解释计算和编程的基础知识方面尤其详尽,并且还阐明了关于 Haskell 的一些精确细节。将来可能会将此页面中的一些元素合并回主书中。


Haskell 是一种编程语言。如果您能够编写和理解 Haskell,则可以创建新的计算机程序,并理解和修改其他人编写的程序。学习编程是一项相当复杂的任务,但 Haskell 是一种很好的入门方式,因为它相当简单且可预测。即使您最终用其他语言进行大部分编程,您的大部分知识也将得以保留。然而,Haskell 不仅仅是一种入门语言;它也是最先进和功能强大的语言之一。

Haskell 软件

[编辑 | 编辑源代码]

要开始使用任何编程语言,您将需要一些特殊的软件来构成您的开发工具链。至少,您需要一个编译器或一个解释器

首先,我们需要揭示一些关于计算机如何工作的知识。您可能听说过 CPU(中央处理器)。它们是硬件部件,负责解释存储在计算机内存中的称为机器语言的数据。机器语言编码简单的指令,当由 CPU 处理时,会导致计算机执行有用的操作,例如将您带到此网页。换句话说,它是一种编程语言。您的计算机执行的程序及其操作的数据以相同的方式存储。

这种架构的一个重要结果是程序可以编写其他程序。这就是解释器和编译器的工作原理。它们将用 Haskell 等语言编写的程序翻译成用机器语言编写的程序,然后计算机可以直接执行这些程序。

编译器和解释器之间的区别在于软件的内部工作原理,您不必过多担心它。如今,这种区别变得越来越模糊和无关紧要。[1]

对于 Haskell,我们使用Glasgow Haskell 编译器 (GHC)。它是一个免费/自由/开源程序,适用于所有主要操作系统。下载

REPL:使用 Haskell 作为计算器

[编辑 | 编辑源代码]

GHC 包含一个称为 GHCi 或“GHC 交互式”的程序。此程序允许您在一行中键入小的 Haskell 程序,并在您按下 Enter 键时执行它们。请查阅GHC 文档以获取有关如何启动 GHCi 的信息,并执行此操作。

您应该看到一个提示符,例如 Main>,出现在您面前。(如果它显示其他内容,例如 Prelude>,请不要惊慌)。当 GHCi 处于此状态时,它已准备好接受并执行程序。尝试键入以下简单程序

1 + 1

GHCi 应该通过显示数字 2 来响应。实际上,这就是此程序的目的:计算 1 和 1 的和,并显示它。计算事物并显示它们的程序称为表达式,它们构成了 Haskell 的大部分内容。当您执行表达式以确定它产生的值时,它被称为评估该表达式。

请注意,一旦您评估了表达式,您就可以做的不仅仅是显示它;实际上,表达式产生的值有多种用途。我们将在后面遇到这些不同的技术。现在,只需意识到它们的存在即可。

回到不太理论的问题上,此时,您与 GHCi 的交互应该如下所示

Main> 1 + 1
2

在显示示例时,我们也将以这种方式显示与 GHCi 的交互。程序通常会与其结果一起包含,并在 Main> 提示符之前。您可以通过在提示符上键入程序来复制与 GHCi 相同的交互。

更多算术运算

[编辑 | 编辑源代码]

您可能已经猜到,Haskell 支持全套算术运算符;加法 (+)、减法 (-)、乘法 (*) 和除法 (/)。数字可以用通常的方式表示为整数或实数,但有一个例外;任何数字都不能在小数点前不写数字。例如,.5 应始终写为 0.5。运算符遵循正常的运算顺序,并且可以像往常一样使用括号。您还可以使用各种常量和函数,例如 pisqrtlogsincostan

继续尝试一些表达式;如果您弄错了,您不会破坏任何东西。以下是一些简单的示例

Main> 3 * 3 * 3
27
Main> 9.6
9.6
Main> 5 / 7
0.7142857142857143
Main> sqrt(2)
1.4142135623730951
Main> (7 - 5) * 3
6
Main> cos(pi)
-1.0

请注意,示例中的空格不是必需的;第一个可以写成 3*3*3,第五个可以写成 (7-5)*3,等等。通常,Haskell 对空格并不挑剔,除非另有说明。但是,如有疑问,请使用更多空格,而不是更少空格。

任何计算机用户都熟悉错误。您在编程时也会遇到错误。尝试一下这个小实验

Main> 1 +
<interactive>:1:3: parse error (possibly incorrect indentation)

当您在程序中犯错误时,GHCi 会通知您,并告诉您它认为存在的问题。不幸的是,它并不总是猜对,并且它通常对其诊断的描述相当隐晦。在此示例中,除了关于缩进的无用注释外,它是在正确的轨道上,但它仍然存在后一个问题,即描述相当隐晦。那么,“解析错误”是什么呢?

一个解析器是编译器的一部分,负责将程序分解成逻辑块,并将其转换为适合转换为机器语言的格式。“解析错误”是指编译器无法理解您的程序;这意味着您的程序存在问题。

那么,<interactive>:1:3: 是什么意思呢?第一部分,<interactive>,表示它正在报告您刚刚键入的程序的错误。[2] 第一个数字是发生错误的行号;对于交互式输入的程序,它始终为 1,因此,同样,这对我们来说没有太大用处。第二个是 GHC 认为错误代码段所在的列(实际上,第一列编号为零,因此它是违规代码的列号减 1)。这里它指向第四列,紧跟在加号之后;这是正确的,因为问题是我们省略了运算符的右操作数。(顺便说一句,一般来说,不要将这些数字视为金科玉律;GHC 有时会给您稍微偏离的数字。)

现在,GHC 猜错问题时会发生什么情况?让我们考虑一下上面错误的一个稍微修改后的版本

Main> (1 +)

Top level:
    No instance for (Show (a -> a))
      arising from use of `print' at Top level
    Probable fix: add an instance declaration for (Show (a -> a))
    In a 'do' expression: print it

我们做了一个小改动,错误消息完全改变了。此外,该消息现在诊断的问题与实际问题完全不同,并且提到了您尚未学习的 Haskell 的相当高级的功能。不幸的是,一大类错误会产生这样的消息;处理这些消息是学习 Haskell 的一个更具挑战性的方面。(其他编程系统也存在此问题,但大多数比 Haskell 好一些。)

当然,这是一个极端的例子;大多数情况下,消息不会像这个例子这样偏离目标。

尝试键入一些无效的程序,看看您会收到什么类型的错误消息,以便对它们有所了解。再次强调,您不会破坏任何东西,所以不用担心。以下是一些示例

Main> xxx
<interactive>:1:0: Not in scope: `xxx'
Main> .5
<interactive>:1:0: parse error on input `.'
Main> 5*.5
<interactive>:1:1: Not in scope: `*.'
Main> &$#^!
<interactive>:1:0: parse error on input `&$#^!'
Main> sqrt sqrt 4
<interactive>:1:0:
    No instance for (Floating (a -> a))
      arising from use of `sqrt' at <interactive>:1:0-3
    Probable fix: add an instance declaration for (Floating (a -> a))
    In the definition of `it': it = sqrt sqrt 4
Main> sqrt(sqrt(4))
1.4142135623730951

Haskell 支持称为变量的功能。这些类似于代数中的变量,但对它们的用法有更多限制。

变量由一个或多个字母命名;xjanerespondxyzzy都是变量的有效名称。它们也可以包含大写字母,但不能作为第一个字母;例如,Jane不是有效的变量名,而jAnE是。大写字母的典型用法是分隔名称中的单词;例如,通常编写multiWordVariableName而不是编写multiwordvariablename,这样更容易阅读。

您可以使用以下形式的程序为变量赋值

 let variable name = value

例如,程序let x = 5x定义为五。值也可以是任意表达式,例如(2 + 3) / 1

请注意,变量名是唯一可以出现在等号左侧的内容。以下程序都无法正确工作

Main> let 5 = x
Main> let 2 * x = 5
Main> let x = x

奇怪的是,GHCi 对后两个程序没有生成任何错误。这是因为它们是有效的 Haskell 代码;只是没有达到您的预期效果。例如,在执行第二个语句后,2 * 7 的值为五,并且,在执行第三个语句后,尝试查找x的值会导致 GHCi 挂起。(在 UNIX 中按 ctrl+c,在 Mac OS X 中按 ctrl+.,在 Windows 中按 ctrl+break 以停止它。)

底线:Haskell 不是严格意义上的代数;不要将其当作代数来使用。

一旦您确定了变量的值,您就可以在后续表达式中使用该变量;变量的每次出现都将被替换为其值。例如

Main> let x = 5
Main> let y = 2 + 2
Main> let z = sqrt 9
Main> x * (y + z)
35.0

多个变量可以在一个let中使用分号分隔赋值来建立。例如,前面的示例可以改写为

Main> let x = 5; y = 2 + 2; z = sqrt 9
Main> x * (y + z)
35.0

虽然您已经掌握了相当多的内容,但您可能觉得离用计算机编程的目标还很远。我们到目前为止编写的程序并没有完成很多工作;您用纸和笔或传统的袖珍计算器也能做到同样的事情。在本节结束时,您将能够做一些不太琐碎的事情,但示例仍然会非常人为。但是,此时,您别无选择,只能向上发展;您可以编写的程序的多样性和复杂性将从这里开始呈指数级增长,并在接下来的几个章节中持续增长,因为您将学习新的表达式类型以及组合表达式的新方法。

Haskell 支持函数。首先,不要假设这个词与数学函数相关的任何先入为主的观念;Haskell 的函数有点不同。

与 Haskell 函数更匹配的是 Haskell 变量。变量代表一个表达式。函数也代表一个表达式,但有一个变化:它接受一个参数;一个在函数内部定义的变量,在使用函数时提供。这个概念也许可以通过示例来最好地理解

Main> let f x = x + 3

此代码定义了函数f,其参数为x

我们能用f做什么?我们可以将其应用于一个参数。假设参数为 4。此代码为

Main> f 4
7

这里发生了什么?让我们回顾一下f的定义

let f x = x + 3

表达式f 4f的定义替换,其中x被替换为四。因此,它变成了4 + 3,当然,结果是 7。

函数定义的一般形式为

 let function argument = definition

函数应用的一般形式为

 function argument

请注意,在这些定义中,“参数”一词以两种不同的方式使用。第一种是函数与其定义相关联的类型;只是一个代表值的名称,在应用函数时提供。第二种是在应用函数时取代先前类型参数的实际值,从而产生可以计算的表达式。

再举几个例子

Main> let reciprocal n = 1 / n
Main> reciprocal 5
0.2
Main> let theSame thing = thing
Main> theSame 6
6
Main> let funny joe = log(pi / joe) * cos(joe + 3)
Main> funny 6
0.5895282337509272

同样,有一种简单的技巧可以手动计算函数展开的值

  1. 写下函数的展开式。
  2. 再次写下它,将参数名称的所有出现都替换为参数值。
  3. 计算结果表达式。

带有多个参数的函数

[编辑 | 编辑源代码]

函数表示法表明可以创建具有多个参数的函数。事实上,这是可能的,并且完全按照您的预期工作

Main> let add x y = x + y
Main> add 5 6
11
Main> let average x y z = (x + y + z) / 3
Main> average 5 6 7
6
Main> let first a b = a
Main> first 8 1
8

在使用多参数函数进行编程时,需要注意的一个“陷阱”是向函数传递过多参数或参数不足。以下示例演示了此问题

Main> average 1 2
Top level:
    No instance for (Show (a -> a))
      arising from use of `print' at Top level
    Probable fix: add an instance declaration for (Show (a -> a))
    In a 'do' expression: print it
Main> average 1 2 3 4
<interactive>:1:0:
    No instance for (Fractional (t -> a))
      arising from use of `average' at <interactive>:1:0-6
    Probable fix: add an instance declaration for (Fractional (t -> a))
    In the definition of `it': it = average 1 2 3 4

这两个错误看起来非常相似。通常,如果您遇到此类错误,请检查您是否向函数传递了正确数量的参数。

表达式中的函数

[编辑 | 编辑源代码]

到目前为止,我们只将数字作为函数的参数。但是,您也可以将表达式作为参数,并将函数应用用作表达式

Main> let f x = 2 * x
Main> f (1 + 1)
4
Main> f (f (f 3))
24
Main> f 4 + 2
10

函数应用在运算符之前计算;因此,f 4 + 2等价于(f 4) + 2,而不是f (4 + 2).

由于函数应用是表达式,因此它们可以在函数定义中使用。例如

Main> let f x = 2 * x; g x = 3 + f x
Main> f 5
10
Main> g 5
13

这里有几点需要注意

  • 我们在let表达式中使用了分号表示法来压缩两个函数的定义;f x = 2 * xg x = 3 + f x.
  • fg之前定义,但不必如此;如果颠倒定义顺序,程序仍然可以工作。但是,以下代码无法工作
Main> let g x = 3 + f x
<interactive>:1:14: Not in scope: `f'

(当 GHC 抱怨变量或函数“不在作用域内”时,它只是意味着它还没有看到它的定义。如前所述,GHCi 要求在使用变量和函数之前对其进行定义。)

  • 的定义g, 3 + f x等价于3 + (f x),如前面给出的规则所规定。因此,g 5变为3 + (2 * 5)在展开gf的定义之后;该表达式进一步计算得到 13。

条件测试

[编辑 | 编辑源代码]

我们承诺,随着您阅读本书的更多内容,您将学习编写更新、更令人兴奋的程序类型。在本节中,我们将填补拼图中的另一块缺失的部分。

您到目前为止编写的程序似乎有些简单;它们无法做出选择,在不同时间做不同的事情。一种易于实现的简单决策是使用if表达式。这些表达式的形式如下

 if test then expression else expression

测试部分可以采用以下形式之一

 expression == expression
 expression /= expression
 expression < expression
 expression > expression
 expression <= expression
 expression >= expression

每种形式描述了两个表达式之间某种类型的关系;例如,<是数学上的小于测试。每个都是某个数学运算符的纯文本形式

== 等于测试。它使用双等号来区分它与在let表达式中使用的等号。
/= 不等于测试。它是的变体。
<, > 小于和大于测试。
<=, >= 小于等于和大于等于测试。是的变体。

当一个if表达式被计算时,如果测试部分声明的内容为真(例如5 < 6),则它计算为then表达式;否则,如果测试部分声明的内容为假(例如5 == 6),则它计算为else表达式。以下示例演示了这一点

Main> let x = 5
Main> if x < 7 then 1 else 2
1
Main> if x <= 5 then x + 1 else pi
6
Main> 1 + (if 2 * x == 10 then 2 else 1)
3
Main> if x < 6 then (if x < 5 then 1 else 2) else 3
2
Main> if x < 5 then 1 else (if x < 6 then 2 else 3)
2

请注意,在最后三个示例中,括号不是必需的;但是,如果最后一个示例是用加号运算符的操作数反转来编写的,则括号将是必需的;因此,它将是

(if 2 * x == 10 then 2 else 1) + 1

如果在没有括号的情况下编写它,它将计算为

Main> if 2 * x == 10 then 2 else 1 + 1
2

以下是一些使用if表达式中使用的等号。

绝对值

[编辑 | 编辑源代码]
Main> let abs x = if x < 0 then -x else x
Main> abs 5
5
Main> abs (-3)
3
Main> abs 0
0

数值三路测试

[编辑 | 编辑源代码]
Main> let nif x p z n = if x > 0 then p else if x == 0 then z else n
Main> nif 3 1 2 3
1
Main> nif 0 1 2 3
2
Main> nif (-6) 1 2 3
3

此函数对应于以下数学函数

最后的测试,而不是如果 x < 0 则 n,只是否则 n。这是因为,根据数值关系的定义,如果x既不大于也不等于零,则它必须大于零,因此测试将始终为真。此外,每个if都需要一个else子句,你会在永远无法到达的else子句中放什么?

实际上,Haskell 有一个用于这些情况的工具;undefined。这是某个特殊值的名称,当作为表达式结果产生时,只需标记错误。它是在表达式“不可能”的情况下放置的良好值if表达式中使用的等号。

Main> let sign x = nif x 1 0 -1
Main> sign 5
1
Main> sign 0
0
Main> sign (-8)
-1

此函数对应于以下数学函数

我们可以这样定义它

let sign x = if x > 0 then 1 else if x == 0 then 0 else -1

然而,由于nif结果是符号函数的推广,将其定义为nif的应用更简单。事实上,将nif扩展到函数体,您将得到与上述代码完全相同的代码!

逻辑连接

[编辑 | 编辑源代码]

布尔表达式和谓词

[编辑 | 编辑源代码]

参考文献

[编辑 | 编辑源代码]
  1. 例如:如果解释器真正做的事情只是将表达式编译成机器代码并非常快速地执行它,那么解释器是否为解释器?鉴于w:部分求值,解释和编译之间的理论差异似乎最小或不存在。
  2. 还有其他程序输入方法(我们现在忽略这些方法),这些方法将在此处显示其他内容。
华夏公益教科书