Haskell/入门
注意
此页面不是 Haskell Wikibook 的主要部分。它与主要的入门材料有重叠。因为从多个角度体验事物是学习的最佳方式,所以阅读本文作为补充以加强基本概念可能会有用。
此页面在解释计算和编程的基础知识方面尤其详尽,并且还阐明了关于 Haskell 的一些精确细节。将来可能会将此页面中的一些元素合并回主书中。
Haskell 是一种编程语言。如果您能够编写和理解 Haskell,则可以创建新的计算机程序,并理解和修改其他人编写的程序。学习编程是一项相当复杂的任务,但 Haskell 是一种很好的入门方式,因为它相当简单且可预测。即使您最终用其他语言进行大部分编程,您的大部分知识也将得以保留。然而,Haskell 不仅仅是一种入门语言;它也是最先进和功能强大的语言之一。
要开始使用任何编程语言,您将需要一些特殊的软件来构成您的开发工具链。至少,您需要一个编译器或一个解释器。
首先,我们需要揭示一些关于计算机如何工作的知识。您可能听说过 CPU(中央处理器)。它们是硬件部件,负责解释存储在计算机内存中的称为机器语言的数据。机器语言编码简单的指令,当由 CPU 处理时,会导致计算机执行有用的操作,例如将您带到此网页。换句话说,它是一种编程语言。您的计算机执行的程序及其操作的数据以相同的方式存储。
这种架构的一个重要结果是程序可以编写其他程序。这就是解释器和编译器的工作原理。它们将用 Haskell 等语言编写的程序翻译成用机器语言编写的程序,然后计算机可以直接执行这些程序。
编译器和解释器之间的区别在于软件的内部工作原理,您不必过多担心它。如今,这种区别变得越来越模糊和无关紧要。[1]
对于 Haskell,我们使用Glasgow Haskell 编译器 (GHC)。它是一个免费/自由/开源程序,适用于所有主要操作系统。下载
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
。运算符遵循正常的运算顺序,并且可以像往常一样使用括号。您还可以使用各种常量和函数,例如 pi
、sqrt
、log
、sin
、cos
和 tan
。
继续尝试一些表达式;如果您弄错了,您不会破坏任何东西。以下是一些简单的示例
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 支持称为变量的功能。这些类似于代数中的变量,但对它们的用法有更多限制。
变量由一个或多个字母命名;x
、jane
、respond
和xyzzy
都是变量的有效名称。它们也可以包含大写字母,但不能作为第一个字母;例如,Jane
不是有效的变量名,而jAnE
是。大写字母的典型用法是分隔名称中的单词;例如,通常编写multiWordVariableName
而不是编写multiwordvariablename
,这样更容易阅读。
您可以使用以下形式的程序为变量赋值
let variable name = value
例如,程序let x = 5
将x
定义为五。值也可以是任意表达式,例如(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 4
被f
的定义替换,其中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
同样,有一种简单的技巧可以手动计算函数展开的值
- 写下函数的展开式。
- 再次写下它,将参数名称的所有出现都替换为参数值。
- 计算结果表达式。
函数表示法表明可以创建具有多个参数的函数。事实上,这是可能的,并且完全按照您的预期工作
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 * x和g x = 3 + f x.
- f在g之前定义,但不必如此;如果颠倒定义顺序,程序仍然可以工作。但是,以下代码无法工作
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)在展开g和f的定义之后;该表达式进一步计算得到 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扩展到函数体,您将得到与上述代码完全相同的代码!