Skip to main content

第 2 章:初步实践(First steps)

原文:Graham Hutton, Programming in Haskell, Second Edition, Chapter 2。维护者已确认本书可翻译并发布用于学习研究。

本章带我们正式迈出 Haskell 编程的第一步。我们先介绍 GHC 系统和标准 Prelude,然后说明函数应用的记法,编写第一个 Haskell 脚本,最后讨论脚本中的若干语法约定。

2.1 Glasgow Haskell Compiler

上一章已经看到,小型 Haskell 程序可以手工执行。但在实践中,我们通常需要一个能够自动执行程序的系统。本书使用 Glasgow Haskell Compiler,这是一个先进的开源 Haskell 实现。

这个系统主要包含两个部分:批处理编译器 GHC,以及交互式解释器 GHCi。本书主要使用解释器,因为它的交互式特性非常适合教学和原型开发,而且性能足以满足书中大多数应用。不过,如果需要更高性能,或者需要生成独立可执行版本的 Haskell 程序,也可以使用编译器本身。例如,在第 9 章和第 11 章的扩展编程示例中,我们会使用编译器。

2.2 安装与启动(Installing and starting)

Glasgow Haskell Compiler 可以从 Haskell 主页 haskell.org 免费下载,支持多种操作系统。对于初学者,建议下载 Haskell Platform,它提供了安装系统和一组常用库的便捷方式。更有经验的用户可能更愿意手动安装系统和库。

安装完成后,可以在终端命令提示符(例如 $)处输入 ghci 来启动交互式 GHCi 系统:

$ ghci

如果一切正常,系统会显示欢迎信息:

GHCi, version A.B.C: http://www.haskell.org/ghc/
:? for help
Prelude>

GHCi 提示符 > 表示系统正在等待用户输入要计算的表达式。例如,可以像使用计算器一样计算简单的数字表达式:

> 2+3*4
14
> (2+3)*4
20
> sqrt (3^2 + 4^2)
5.0

按照通常的数学约定,Haskell 中的乘方优先级高于乘法和除法,而乘法和除法又高于加法和减法。例如,2*3^4 表示 2*(3^4),而 2+3*4 表示 2+(3*4)。此外,乘方是向右结合的,而另外四个主要算术运算符向左结合。例如,2^3^4 表示 2^(3^4),而 2-3+4 表示 (2-3)+4。不过在实践中,使用显式括号通常比依赖这些规则更清楚。

2.3 标准 Prelude(Standard prelude)

Haskell 自带大量内置函数,它们定义在一个称为标准 Prelude 的库文件中。除了熟悉的数字函数(例如 +*)之外,Prelude 还提供了一系列作用于列表的有用函数。在 Haskell 中,列表元素写在方括号中,并用逗号分隔,例如 [1,2,3,4,5]。下面展示一些最常用的列表库函数。

选择非空列表的第一个元素:

> head [1,2,3,4,5]
1

移除非空列表的第一个元素:

> tail [1,2,3,4,5]
[2,3,4,5]

选择列表中的第 n 个元素(从零开始计数):

> [1,2,3,4,5] !! 2
3

选择列表的前 n 个元素:

> take 3 [1,2,3,4,5]
[1,2,3]

移除列表的前 n 个元素:

> drop 3 [1,2,3,4,5]
[4,5]

计算列表长度:

> length [1,2,3,4,5]
5

计算数字列表的和:

> sum [1,2,3,4,5]
15

计算数字列表的乘积:

> product [1,2,3,4,5]
120

连接两个列表:

> [1,2,3] ++ [4,5]
[1,2,3,4,5]

反转列表:

> reverse [1,2,3,4,5]
[5,4,3,2,1]

附录 B 给出了标准 Prelude 中一些最常用定义,可作为实用参考。

2.4 函数应用(Function application)

在数学中,把函数应用到参数通常写成用圆括号包围参数,而两个值的乘法通常通过把两个值并排写在一起来隐式表示。例如,在数学中,表达式

f(a,b) + c d

表示把函数 f 应用到两个参数 ab,并把结果加上 cd 的乘积。由于函数应用在 Haskell 中处于核心地位,Haskell 使用空格来隐式表示函数应用,而两个值的乘法则用操作符 * 显式表示。因此,上面的表达式在 Haskell 中写作:

f a b + c*d

此外,函数应用的优先级高于语言中的所有其他操作符。例如,f a + b 表示 (f a) + b,而不是 f (a + b)。下表给出一些例子,展示数学记法与 Haskell 中函数应用记法的差异:

数学Haskell
f(x)f x
f(x,y)f x y
f(g(x))f (g x)
f(x,g(y))f x (g y)
f(x)g(y)f x * g y

注意,上表中的 Haskell 表达式 f (g x) 仍然需要括号,因为单独写 f g x 会被解释为把函数 f 应用到两个参数 gx,而这里的意图是把 f 应用到一个参数,即把函数 g 应用到参数 x 后得到的结果。表达式 f x (g y) 也有类似原因。

2.5 Haskell 脚本(Haskell scripts)

除了标准 Prelude 提供的函数,也可以定义新的函数。新函数定义在脚本中,脚本是一个由一系列定义组成的文本文件。按照约定,Haskell 脚本的文件名通常使用 .hs 后缀,以便与其他类型的文件区分。这并不是强制要求,但有助于识别文件。

我的第一个脚本(My first script)

开发 Haskell 脚本时,通常很有用的一种做法是保持两个窗口打开:一个运行脚本编辑器,另一个运行 GHCi。假设我们启动文本编辑器,输入下面两个函数定义,并把脚本保存到名为 test.hs 的文件中:

double x = x + x

quadruple x = double (double x)

接着,假设我们保持编辑器打开,在另一个窗口启动 GHCi,并让它加载这个新脚本:

$ ghci test.hs

现在,标准 Prelude 和脚本 test.hs 都已加载,来自两者的函数都可以自由使用。例如:

> quadruple 10
40
> take (double 2) [1,2,3,4,5]
[1,2,3,4]

再假设我们保持 GHCi 打开,回到编辑器,把下面两个函数定义添加到已有内容后面,并重新保存文件:

factorial n = product [1..n]

average ns = sum ns `div` length ns

我们也可以把第二个函数定义为 average ns = div (sum ns) (length ns),但把 div 写在两个参数之间更加自然。一般来说,任何带两个参数的函数,都可以通过把函数名包在一对反引号中,写在两个参数之间。

GHCi 不会在脚本被修改时自动重新加载它,因此在使用新定义之前,必须执行重新加载命令:

> :reload
> factorial 10
3628800
> average [1,2,3,4,5]
3

作为参考,图 2.1 中的表格总结了一些最常用 GHCi 命令的含义。注意,任何命令都可以缩写为它的首字母。例如,:load 可以缩写为 :l。命令 :set editor 用于设置系统使用的文本编辑器。例如,如果希望使用 vim,可以输入 :set editor vim。命令 :type 会在下一章更详细地说明。

命令含义
:load name加载脚本 name
:reload重新加载当前脚本
:set editor name将编辑器设置为 name
:edit name编辑脚本 name
:edit编辑当前脚本
:type expr显示表达式 expr 的类型
:?显示所有命令
:quit退出 GHCi

图 2.1 常用 GHCi 命令(Useful GHCi commands)

命名要求(Naming requirements)

定义新函数时,函数名及其参数名必须以小写字母开头,后面可以跟零个或多个字母(小写或大写均可)、数字、下划线和右单引号。例如,下面都是有效名称:

myFun
fun1
arg_2
x'

下面这组关键字在语言中具有特殊含义,不能用作函数或函数参数的名字:

case      class     data      default   deriving
do else foreign if import in
infix infixl infixr instance let
module newtype of then type where

按照约定,Haskell 中的列表参数名通常带有后缀 s,表示它们可能包含多个值。例如,数字列表可以命名为 ns,任意值列表可以命名为 xs,字符列表的列表可以命名为 css

布局规则(The layout rule)

在脚本中,处于同一层级的每个定义都必须从完全相同的列开始。这条布局规则使得系统可以根据缩进确定定义的分组。例如,在下面脚本中:

a = b + c
where
b = 1
c = 2
d = a * 2

从缩进可以清楚看出,bc 是供 a 的主体使用的局部定义。如果愿意,也可以通过用花括号包围一组定义,并用分号分隔每个定义,来显式表示这种分组。例如,上面的脚本也可以写作:

a = b + c
where
{b = 1;
c = 2};
d = a * 2

甚至可以合并到一行中:

a = b + c where {b = 1; c = 2}; d = a * 2

不过,一般来说,通常更推荐依赖布局规则来确定定义分组,而不是使用显式语法。

制表符(Tabs)

制表符可能给脚本带来问题,因为布局在 Haskell 中具有意义,而不同文本编辑器解释制表符的方式并不相同。因此,建议在缩进定义时避免使用制表符,而且 GHC 系统在检测到制表符时会给出警告。如果确实希望在脚本中使用制表符,最好配置编辑器,让它自动把制表符转换为空格。Haskell 假定制表位宽度为 8 个字符。

注释(Comments)

除了新定义,脚本中也可以包含会被编译器忽略的注释。Haskell 支持两种注释,称为普通注释和嵌套注释。普通注释以符号 -- 开始,并延伸到当前行末尾,如下面例子所示:

-- Factorial of a positive integer:
factorial n = product [1..n]

-- Average of a list of integers:
average ns = sum ns `div` length ns

嵌套注释以符号 {- 开始,以 -} 结束,可以跨越多行,并且可以嵌套,也就是说,注释中可以包含其他注释。嵌套注释特别适合临时从脚本中移除某些定义,例如:

{-
double x = x + x

quadruple x = double (double x)
-}

2.6 章注(Chapter remarks)

除了 GHC 系统,haskell.org 还包含大量与 Haskell 有关的其他有用资源,包括社区活动、语言文档和新闻条目。

2.7 练习(Exercises)

  1. 使用 GHCi 实际运行本章中的示例。
  2. 为下面的数字表达式加上括号:
2^3*4
2*3+4*5
2+3*4^5
  1. 下面的脚本包含三个语法错误。修正这些错误,然后使用 GHCi 检查脚本是否能正常工作。
N = a `div` length xs
where
a = 10
xs = [1,2,3,4,5]
  1. 库函数 last 用于选择非空列表的最后一个元素;例如,last [1,2,3,4,5] = 5。说明如何使用本章介绍的其他库函数来定义函数 last。你能想到另一种可能的定义吗?
  2. 库函数 init 用于移除非空列表的最后一个元素;例如,init [1,2,3,4,5] = [1,2,3,4]。说明如何用两种不同方式类似地定义 init

练习 2-4 的解答见附录 A。