Yet_Another_Haskell_Tutorial

PerlChina
Yet_Another_Haskell_Tutorial

前一段时间为了看 autrijus 的 Pugs 代码,所以开始学 Haskell,可是用 google 搜索了好半天,只居然只从网上搜到了一篇 Haskell 中文资料。
无奈之中,只好求助于 autrijus,autrijus 告诉我应该看 Yet Another Haskell Tutorial,所以就找来看看喽,顺便翻译出来给大家看看。

Yet Another Haskell Tutorial
原文出处: http://www.isi.edu/~hdaume/htut/
原文作者: Hal Daume III
翻译者: flw
关键字: Haskell 中文教程 中文资料

致各位网友:如有评论,请贴在本文最前面,不要贴在翻译正文中,不然有可能会被覆盖掉。

                              另一本 Haskell 教程
                         (Yet Another Haskell Tutorial)

                                 Hal Daume III

                        http://www.isi.edu/~hdaume/htut/

                       版权所有 Hal Daume III, 2002-2004

声明

    这个教程的 preprint 版打算让整个 Haskell 社区都可用,所以我们给每个人都授予
拷贝和发布的权利,当然你得保证本书的完整性,并且包括这个声明在内。

    对本书的任何修改未经过作者同意均不能发表,作者同意后,可以在包含本声明的
前提下发表。

    本书的作者保留随时修改本书版权和不再长期免费提供本书的权利。

译者注:本教程之所以叫做《另一本 Haskell 教程》,是因为之前已经有一本《Haskell
教程》了。

关于本书

    本书的目标是向读者提供一个完整的 Haskell 编程手册。读者不需要任何有关
Haskell 编程或者函数型语言编程的知识便可以阅读此书。当然了,如果读者熟悉编程有关
的一些基础知识(例如算法等等),那么将很有帮助。

    本书不打算做有关编程的一般知识的介绍,它只讲述 Haskell 语言编程的有关细节。

    为了便于学习,你必须足够熟悉你的操作系统和一种文本编辑器。另外,本书只讨论在
Windows 下和 *Nix 下的 Haskell 编程。其它平台不予考虑,如有问题请参阅你的操作系
统的相关文档。

什么是 Haskell?

    Haskell 是一种惰性的、纯粹的函数型编程语言。

    Hakell 之所以被称作是“惰性的语言”的原因,是因为对于“寻找问题的答案”来讲
没有帮助的表达式是不被计算的。 和“惰性的”相反,大多数通用编程语言(C、C++、
Java、even ML)的求值是严格的。一个严格的语言是说它的每一个表达式都需要求值,而
不论这个表达式是否重要。(这么说也不完全准确,因为严格语言通常会有一个编译器优化
的过程,可以消去代码中无用的表达式。)

    Haskell 之所以被称作是“纯粹的函数型语言”原因,是因为它不允许副作用
(side effect) 的产生。所谓“副作用”是指影响了环境状态,例如一个函数望屏幕上打印
了一些字符就被认为是“副作用”,就像是一个函数影响了一个全局变量一样。当然了,一
种不产生副作用的语言是没有任何用处的。Haskell 使用一种技术将所有被污染的代码和其
余的程序部分分离出来,用一种安全的方式单独执行。参见第九章“Monads”和第五章“如
何在纯函数型编程语言中进行输入输出”。

    Hakell 之所以被称作是“函数型编程语言”的原因,是因为一个 Haskell 程序的求值
过程看上去和纯粹的数学函数的求值过程是一样的。这和大多数通用编程语言如 C 或者 
Java 等不同,后者将一个程序看作是一个语句的序列,并且一条一条执行它。这种语言通
常被称作是命令式语言。

Haskell 语言的历史

    事情要从头说起。下面是一段引用自《Haskell 98 报告》的文字:
1987 年 9 月,“函数型编程语言和计算机体系(FPCA'87)”研讨会在波兰召开。
在俄勒冈,大家针对目前函数型编程语言的窘境进行了讨论:当时世界上已经产生了十多种
不严格的、纯粹的函数型编程语言。它们拥有类似的语法基础和表达能力。在这次会议中,
与会者一致认为,缺少一种通用的语言规范已经成为大面积推广函数型编程语言的最大障
碍。他们决定有必要成立一个委员会去设计这样一种通用的函数型编程语言。如果事情发展
得更顺利的话,应该成立一个基金会去支持实际的应用程序开发,包括一种能够促进函数型
编程语言的媒介手段。《Haskell 98 报告》发表了委员会的全部努力成功:一种被称作叫
Haskell 的纯粹的函数型编程语言。它以著名逻辑学家 Haskell B. Curry 的名字命名。

    委员会最初的目的是想要设计出满足下面这些要求的语言:
        1,它必须能够适合教学、研究、应用开发,包括一些大系统的构造。
        2,它必须能够使用形式语言来准确描述。
        3,它必须是自由免费的,任何人只要愿意都可以获取、使用和再次发布它。
        4,它必须建立在大家一致认可的基础上。
        5,它应该能够消除目前的函数型编程语言的差异。

    委员会的目标是 Haskell 语言能够成为将来研究语言设计的一个基础,并且希望它的
扩展或变体能够适应将来的各种需求。事实上,从 Haskell 第一次发布以来,它的确在演
变。到了 1997 年中期,Haskell 规范已经进行了 4 次演变(最后的发布称作是 Haskell 
1.4)

    1997 年在阿姆斯特丹举行的 Haskell 研讨会决定,现在需要制定一个稳定的 Haskell
变体。最后,会议报告将这个稳定的 Haskell 变体命令为“Haskell 98”。Haskell 98 对
Haskell 1.4 进行了小小的整理,使之更加单纯,并且舍弃了一些不谨慎的部分。

    最初的 Haskell 标准制定的规范后来发展成了 Haskell 标准库,称作“Prelude”。
另外,大多数程序需要访问大量的库函数(特别象输入输出等和操作系统的简单交互),
如果这些程序需要移植的话,那么又得制定一套标准库。就在 Haskell 98 确定以后的那个
时候,这种需求越来越变得清晰,因此,另一个委员会开始为了完善 Haskell 98 函数库而
努力。

为什么要使用 Haskell?

    首先有一点得确定,那就是在开始学习本教程之前,确保你是因为对 Haskell 感兴趣
而开始使用 Haskell。我个人使用 Haskell 的原因是,相比其它语言来讲,我可以使用
Haskell 在更短的时间内写出更多的没有 BUG 的程序。我也认为它的可读性和延展性非常
好。
    也许更重要的是,我可以在 Haskell 社区中获得令人难以置信的帮助。Haskell 语言
不断地演化,以至于我们甚至都不能说它是“稳定的”,许多非常有用的特性正在被添加
到各种编译器中,并且每一次扩展都会采纳一些用户建议进去。

为什么不使用 Haskell?

    我和大多数我所了解的 Haskell 爱好者共同的两个最大的抱怨是:
        (1) 和 C 等其它语言相比,同样的程序,用 Haskell 写出则运行得更慢一些。
        (2) 和其它语言相比,Haskell 程序几乎无法调试。

    第二个问题几乎不是一个问题,因为我写过的 Haskell 程序很少有 BUG,大多数其它
语言开发中通常碰到的 BUG 在 Haskell 中根本就不存在。而第一个问题我经常碰到。

    然而,和程序员编制程序的时间相比,CPU 的时间更加廉价。我宁愿多等一点儿时间让
计算机去执行程序,也不愿意花上好几天去调试我的程序。当然了,这个观点并不适合所有
的程序。一些人可能会认为 Haskell 的速度简直无法忍受。不过这也没关系,Haskell 拥
有一套机制允许你将其它语言书写的代码链接进来。因此,如果你在 Haskell 里边需要更
快的速度的话,你可以将那部分程序用其它语言来书写,然后在 Haskell 里边链接进来。

    如果在 Haskell 中链接其它语言代码仍然不能够满足你的要求的话,我建议你去看看
O'Caml 语言,它拥有比 C++ 更好的特性,也包括了一些 Haskell 的特点。

什么样的读者适合阅读本书?

    事实上存在很多关于 Haskell 的教程。下面这个网址有一个很完整的清单:
        http://haskell.org/bookshelf (Haskell 书架)
下面对它们做一个简单的介绍:
    ※ 《A Gentle Introduction to Haskell》是一个为已经熟悉函数型编程语言的读者
        准备的 Haskell 介绍。
    ※ 《Haskell Companion》是一个对通用概念和定义的简短介绍。
    ※ 《Online Haskell Course》是一篇用德文书写的 Haskell 快速入门。
    ※ 《Two Dozen Short Lessons in Haskell》(Haskell 二十四学时教程)是一本非
        常好的正规 Haskell 教材。
    ※ 《Haskell Tutorial》是“第三国际暑期学校”(译者注:不知道到底有没有这个
        学校?)规定的《高级函式编程》课程的教材。
    ※ 《Haskell for Miranda Programmers》假设读者已经拥有 Miranda 语言的知识。
    ※ 《PLEAC-Haskell》是一个模仿《Perl Cookbook》风格的教程。

    尽管所有的教程都是非常优秀的,但是他们都不是完整的。《A Gentle Introduction
to Haskell》对于初学者来讲过于深奥而其它的书则过于简单。而且,没有一本书有对输入
输出和交互式程序设计的足够介绍。对于初学者来讲 Haskell 充满着陷阱,即使是你已经
有其它非函数型语言编程的经验也是如此。

    为此,我们需要一本教程,可以让那些不了解函数型编程语言,却了解其它类型的编程
语言的人能够了解 Haskell。本教材不是为初级程序员准备的:它假设你已经了解一些计算
机基础知识和编程知识。(附录中有一些背景信息介绍)

    Haskell 语言在经过 10 年的标准化进程后,最后得到 Haskell 98。因此本书中将以
Haskell 98 作为标准。任何和标准 Haskell 有偏差的地方都会被注明(例如,许多编译器
提供的一些有用的扩展)。

    本书的目标是:
        ※ to be practical above all else 
        ※ 提供一个 Haskell 的全面介绍
        ※ 提出常见的陷阱和它们的解决方案
        ※ 使读者正确判断“如何在现实世界中使用 Haskell”

原作者序:

Acknowledgements
    It would be inappropriate not to give credit also to the original designers of Haskell. Those are: Arvind, Lennart Augustsson, Dave Barton, Brian Boutel, Warren Burton, Jon Fairbairn, Joseph Fasel, Andy Gordon, Maria Guzman, Kevin Hammond, Ralf Hinze, Paul Hudak, John Hughes, Thomas Johnsson, Mark Jones, Dick Kieburtz, John Launchbury, Erik Meijer, Rishiyur Nikhil, John Peterson, Simon Peyton Jones, Mike Reeve, Alastair Reid, Colin Runciman, Philip Wadler, David Wise, Jonathan Young. 
    Finally, I would like to specifically thank Simon Peyton Jones, Simon Marlow, John Hughes, Alastair Reid, Koen Classen, Manuel Chakravarty, Sigbjorn Finne and Sven Panne, all of whom have made my life learning Haskell all the more enjoyable by always being supportive. There were doubtless others who helped and are not listed, but these are those who come to mind.

- Hal Daum'e III

译者自序:

    2005.03.05,我有幸得见唐宗汉先生和各位天南海北的朋友。一群志同道合的人为了学
习、使用、推广 Perl 而从全国各地来到北京。长达 10 个小时的聚会很快就过去了,给我
留下的却是强烈的震撼。所谓井底之蛙,坐井观天,这次聚会,让我得知世界上原来还有这
么一群人以这样一种方式生活着。唐先生随身携带电脑以及等电梯之余、出租车上都要编两
行程序给了我很深刻的印象。鲁迅先生曾说过:“时间就象是海绵里的水一样,要挤总是能
挤出来的”,以前总觉得自己很忙,没有时间做自己想做的事。现在想来,都不过是借口罢
了,lazy 才是真的。

    这次聚会中,唐先生讲述了籍由 Pugs 来帮助 Perl6 开发进程的的思路,令人耳目一
新。我虽不才,也想一览 Pugs 代码。但是 Pugs 是 Haskell 开发的,因此便开始接触
Haskell。

    开始学习 Haskell 的过程中,惊奇地发现 Haskell 有关的中文资料居然非常少。我用
google 仅搜到一篇中文介绍,还算比较全面。无奈,只好寻求唐先生的帮助,于是开始阅
读本书:<<Yet Another Haskell Tutorial>>。并且将自己对本书的理解以中文的形式再写
出来,也算是一种翻译吧。

    在次,我首先要感谢“如飞(rufi)”!他写的《Haskell教程》一文非常的漂亮。正是
因为这篇文章,我才得以了解 Haskell 的概貌。并由此喜欢上 Haskell 这种语言。其次,
我要感谢唐宗汉先生和各位 PerlChina 社区的朋友们!正是他们的帮助,才让我这次翻译
得以顺利地进行。

    由于本人英文水平有限,对 Haskell 又是全然不懂,所以肯定有错误之处,还望各位
朋友不吝赐教!

                                                             岁在乙酉,人在京城
                                                       王兴华(flw) 于 2005 年春

全书目录

第一章  概述

第二章  开始学习 Haskell
        第一节  Hugs
            第一小节    从何获取 Hugs
            第二小节    安装 Hugs
            第三小节    如何运行 Hugs
            第四小节    程序选项
            第五小节    如何获得帮助
        第二节  GHC: Glasgow Haskell Compiler
            第一小节    从何获取 GHC
            第二小节    安装 GHC
            第三小节    如何运行 GHC 编译器
            第四小节    如何运行 GHC 解释器
            第五小节    程序选项
            第六小节    如何获得帮助
        第三节  NHC
            第一小节    从何获取 NHC
            第二小节    安装 NHC
            第三小节    如何运行 NHC
            第四小节    程序选项
            第五小节    如何获得帮助
        第四节  程序文本编辑器

第三章  语言基础
        第一节  算术运算
        第二节  二元组、三元组和多元组
        第三节  列表
            第一小节    字符串
            第二小节    简单列表函数
        第四节  源代码文件
        第五节  函数
            第一小节    Let 绑定
            第二小节    中缀函数
        第六节  代码注释
        第七节  递归
        第八节  交互式编程

第四章  类型基础
        第一节  简单类型
        第二节  泛型
        第三节  类型类
            第一小节    问题的提出
            第二小节    测试是否相等
            第三小节    Num 类
            第四小节    Show 类
        第四节  函数类型
            第一小节    λ演算
            第二小节    高阶类型
            第三小节    烦人的 IO 类型
            第四小节    显式类型声明
            第五小节    函数参数
        第五节  数据类型
            第一小节    二元组
            第二小节    多重构造
            第三小节    递归数据类型
            第四小节    B 树
            第五小节    枚举
            第六小节    Unit 类型
        第六节  CPS(Continuation Passing Style)

第五章  基本输入输出
        第一节  RealWorld 解决方案
        第二节  行为
        第三节  IO 库
        第四节  一个读文件的程序

第六章  模块
        第一节  Exports 指令
        第二节  Imports 指令
        第三节  层次式 Imports
        第四节  Literate Versus Non-Literate
            第一小节    Bird-scripts
            第二小节    LaTeX-scripts

第七章  高级特性
        第一节  部分和中缀操作符
        第二节  本地声明
        第三节  Partial Application
        第四节  模式匹配
        第五节  警戒
        第六节  实例声明
            第一小节    Eq 类
            第二小节    Show 类
            第三小节    其它重要的类
            第四小节    类的上下文
            第五小节    导出类
        第七节  数据类型重访
            第一小节    命名字段
        第八节  更多关于列表
            第一小节    标准列表函数
            第二小节    列表包含
        第九节  数组
        第十节  有限图
        第11节  布局
        第12节  列表中最后的字

第八章  高级类型
        第一节  同义类型
        第二节  新类型
        第三节  数据类型
            第一小节    严格字段
        第四节  类
            第一小节    Pong
            第二小节    计算法
        第五节  例子
        第六节  类别
        第七节  类层次
        第八节  默认

第九章  Monads
        第一节  Do Notation
        第二节  Definition
        第三节  A Simple State Monad
        第四节  Common Monads
        第五节  Monadic Combinators
        第六节  MonadPlus
        第七节  Monad Transformers
        第八节  Parsing Monads
            第一小节    A Simple Parsing Monad
            第二小节    Parsec

第十章  高级技术
        第一节  异常
        第二节  可变数组
        第三节  可变引用
        第四节  The ST Monad
        第五节  合作
        第六节  正则表达式
        第七节  动态类型

第11章  Theory
        第一节  Bottom

第12章  进一步的信息

附录 A Brief Complexity Theory
附录 B Recursion and Induction
附录 C "RealWorld" Examples
附录 D Solutions To Exercises

                                  第一章 概述

    本书发布时含有大量的例子代码,如果不幸你的副本中没有,你也可以访问 Haskell 
的官方网站 http://haskell.org/ 来获取它们。在本书中,例子程序用以下的样式和其余
的文本区分开来:(作者原著 PDF 格式中,使用不同的颜色框来区分,本文因为只有文本
格式,所以只好用实线和虚线来区分了。)

   ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
   ┃ print "Hello, world" ( 用粗实线框来表示 )                            ┃
   ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

和操作系统的输入输出交互部分的文字则用这种样式来表达:

   ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
   ┇ % cd /usr/home/flw   ( 用粗虚线框来表示 )                            ┇
   ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

在本书中,经常会出现一些提示信息。这些信息或者是表示和其它的语言做比较,或者是提
供一些帮助信息。这些提示信息通常以下面的样式出现:

   ┌───────────────────────────────────┐
   │【注意】 提示信息用细实线框包起来                                     │
   └───────────────────────────────────┘

如果出现了一些令人难以理解的部分,我们将会放置一个警告来提醒您的注意:

   ┌┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┐
   ┊【警告】 警告信息用细虚线包起来                                       ┊
   └┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┘

临了再说明一下,我们有时候会提及到内建函数(也可以叫做前奏函数)。内建函数看上去
是这样子的:

    map :: (a->b) -> [a] -> [b]

                            第二章 开始学习 Haskell

    目前存在有三个 Haskell 系统: Hugs、GHC 和 NHC。Hugs 仅仅只是一个解释器,换句
话说,你不能使用 Hugs 编译出可以脱离 Hugs 运行的独立程序,不过你可以用 Hugs 来调
试你的程序,Hugs 是一个优秀的解释环境。GHC 可以以解释和编译两种方式工作。GHC 既
可以像 Hugs 一样工作,也可以将你的程序编译成可以脱离 GHC 环境执行的独立程序。
NHC 则是一个专门的编译器。
    至于选用哪一个工具是你自己的事情,我只在下面的清单中对它们做一个尽量全面的
比较:

    Hugs - 速度非常快。实现了几乎所有的 Haskell 98 语法,并且支持许多扩展。
           内建有模块浏览支持。不能生成独立运行的程序。Hugs 是用 C 语言书写的。
           可以工作在几乎所有的平台。内建有图形库支持。

    GHC -  它的解释器环境要比 Hugs 慢一些,但是允许定义函数(如果是在 Hugs 下,
           那么你必须修改源文件,然后 reload 一下)。GHC 支持所有的 Haskell 98 
           语法及其扩展。有良好的和其它语言的接口。
           GHC 可以说是 Haskell “实际上”的标准。

    NHC -  很少使用,并且没有交互式环境,只是个编译器而已。不过它生成的可执行文
           件要比 GHC 生成的更小、更快。支持 Haskell 98,也支持一些扩展。

作为我个人来讲,这三种我全都安装了,并且在不同的场合使用。我习惯用 GHC 来编译,
(主要是因为我更加熟悉它),用 Hugs 来交互执行,因为交互式环境下它更快一些。
我的建议是这样。不管怎样,你应该把它们全都下载下来安装一遍,这样可以有一个公平
的评价。

下面分别是关于如何下载、安装这三种产品的一个说明。其中有些内容可能已经过时,请参
考 Haskell 的官方网站 http://www.haskell.org/ 来得到最新的说明。

第 2.1 节   Hugs

    Hugs 支持大多数 Haskell 98 语法,不过它缺少一些库以及许多有用的扩展,包括:
多重参数(multi-parameter)、类型类(type classes)、可变记录(extensible records)、
rank-2 polymorphism, existentials, scoped type variables, 还有 restricted type
synonyms.

第 2.1.1 小节   从何获取 Hugs

    Hugs 的官方主页是:http://haskell.org/hugs。打开这个主页,你可以看到一个写着
"downloading" 的链接,点击它便可以带你进入下载页面。在这里你可以下载到适合你的计
算机的 Hugs 版本。

第 2.1.2 小节   安装 Hugs

    一旦你下载完毕,那么就可以进行安装了。安装 Hugs 和安装其它的应用程序没有什么
太大的不同。

    Windows 平台: 双击打开你下载的 msi 文件就可以开始安装了。
    RPMs 包: 使用你熟悉的 rpm 安装工具安装就可以了。
    源码安装: 你首先要解压、解包,然后 configure/make/make install 就可以了。

    (译者注:实在太简单了,没有什么可说的。总不能截几张屏幕上来吧?)

第 2.1.3 小节   运行 Hugs

    声明一点: 本手册假定你的 Hugs 已经成功安装了。如果没有成功安装,请参考 Hugs
的说明文件。

    在 Unix 主机上,Hugs 解释器通常都是用类似这样一个命令来运行的:

    hugs [option - file ] ...

    在 Windows 主机上,Hugs 可以通过在开始菜单中选择来启动,或者直接双击一个 .hs
或 .lsh 文件来启动。当然了,你可以可以通过在命令行提示符下面输入 hugs 来启动。

    Hugs 使用命令行开关来设置系统参数,命令行参数用 + 或者 - 某一个开关的方法来
定制解释器的环境。Hugs 启动时会依次执行以下任务:

    ◆ 处理环境变量中的开关选项。环境变量 HUGSFLAGS 中可以设置 Hugs 的选项。
       如果是在 Windows 平台的话,注册表中也会有一些开关选项。
       (译者注:在我的机器上是在以下位置:)

       HKEY_CURRENT_USER\Software\Haskell\Hugs\Nov 2003\Options
       HKEY_CURRENT_USER\Software\Haskell\Hugs\WinhugsNov 2003\Options

    ◆ 处理命令行开关选项。

    ◆ 内部数据结构初始化。特别是堆的初始化,堆的大小就是在这个时候确定的。因此
       如果你想要更改堆的大小,那么应该通过命令行选项或者环境变量、注册表等方式
       进行设置。

    ◆ 加载 Prelude 库。Hugs 将在选项 -P 所指示的位置中寻找 Prelude.hs 文件。如
       果在 -P 所指定的位置中以及当前目录中没有找到 Prelude.hs 文件,那么 Hugs 
       会终止。缺少 Prelude 文件 Hugs 将不能运行。

    ◆ 加载命令行中指定的其它程序文件。命令 "hugs f1" 的效果和先启动 hugs 然后再
       输入 ":load f1" 的效果是相同的。
       如果指定了多个文件,Hugs 将逐个加载。如果其中一个出了错,那么

    Hugs 的开关选项在下一节中介绍。

第 2.1.4 小节     程序选项

    鉴于篇幅有限,这里只列举常用的几个 Hugs 开关选项,更多有关的说明请查看 Hugs
的相关说明。

    最重要的一个开关是 "+98" 或者 "-98"。如果你启动 Haskell 时指定了 "+98",那么
将会以 Haskell 98 模式启动,所有的扩展都被禁用。如果启动时指定了 "-98",那么所有
的扩展就会被启用。如果你从网上下载了什么其它代码,但是在加载的时候出了麻烦,那么
请确认你的 "98" 这个开关是否设置正确。

    (译者注: Hal Daume III 只介绍了这一个选项,哪位朋友有兴趣将其余的选项再翻译
一下,可以和我联系)

    关于 Hugs 选项的进一步的信息你可以参考它的手册:

    http://cvs.haskell.org/Hugs/pages/hugsman/started.html

第 2.1.5 小节     如何获得帮助

    想获得关于 Hugs 的帮助,请访问 Hugs 的主页。想获得关于 Haskell 的帮助,请访
问 Haskell 的主页。

第 2.2 节   Glasgow Haskell Compiler(GHC 编译器)

    Glasgow Haskell Compiler (GHC) 是一个稳定的、特性齐全的、优化的编译器。它的
解释器环境对 Haskell 98 的支持非常好。GHC 可以将 Haskell 程序编译成本地机器码或
者 C 程序代码。它提供了很多 Haskell 98 的扩展,例如:concurrency, a foreign lan-
guage interface, multi-prarameter type classes, scoped type variables, existent-
ial and universal quantification, unboxed types, exceptions, weak pointers 等等.
GHC comes with a generational garbage collector, and a space and time profiler.

第 2.2.1 小节   从何获取 GHC

    请到 GHC 官方网站 http://haskell.org/ghc 来下载最新版本。作者在书写本教程时
的最新版本是 5.04.2,译者在翻译本教程时的最新版本是 6.2.2。在 GHC 的网站上,你可
以选择适合你的机器的版本来下载。

第 2.2.2 小节   安装 GHC

    一旦你下载完毕,那么就可以进行安装了。安装 GHC 和安装其它的应用程序没有什么
太大的不同。

    Windows 平台: 双击打开你下载的 msi 文件就可以开始安装了。
    RPMs 包: 使用你熟悉的 rpm 安装工具安装就可以了。
    源码安装: 你首先要解压、解包,然后 configure/make/make install 就可以了。

    更多有关安装 GHC 的信息你可以参考 GHC 用户手册和 "安装 GHC" 有关的章节。

第 2.2.3 小节   如何运行 GHC 编译器

    (译者注:后文第 3.4 节曾提到此处给出了一个 Hello World 程序,但是实际上原文
中并没有,所以译者在此加上)

    首先,像大多数编程语言的入门教程一样,为了演示 GHC 的用法,我们给出一个简单
的 "Hello World" 程序:

  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
  ┃ module Main where                                                    ┃
  ┃ main = putStrLn "Hello World!"                                       ┃
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

    运行 GHC 编译器非常简单。我们假定你将上面的这段代码保存为 Main.hs。那么你可
以像下面这样来编译它:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ % ghc --make Main.hs -o main                                         ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

--make  选项告诉 GHC 这是一个程序,而不是一个模块,并且表示你想编译它,这样 GHC
        会将 Main.hs 关联的模块都一起编译进去。
Main.hs 则是你想要编译的文件名。
-o main 选项的意思是说,你想把编译生成的文件命令为 "main"。

  ┌───────────────────────────────────┐
  │【注意】 在 Windows 平台下生成的文件名默认包含扩展名 .exe,也就是说, │
  │         实际生成的文件名是 main.exe                                  │
  └───────────────────────────────────┘

现在,你就可以通过在命令提示符下输入 "main" 来运行它了。

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ % ./main                                                             ┇
  ┇ "Hello World!"                                                       ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

第 2.2.4 小节   如何运行 GHC 解释器 (GHCi)

    GHC 的解释器环境(GHCi)可以通过 "ghci" 命令或者 "ghc --interactive" 来启动。
在命令行下你可以传递一些模块或者文件名给 GHCi,表示 GHCi 在启动后需要立即将指定
的文件或者模块加载。这和在 GHCi 环境下执行 :load 命令的效果是一样的。

第 2.2.5 小节   程序选项

    鉴于篇幅有限,这里只列举常用的几个 GHC 开关选项,更多有关的说明请查看 GHC 的
相关说明。

    最重要的一个开关是 "--fglasgow-exts"。如果你启动 GHC 时没有指定这个选项,那
么,GHC 将会以 Haskell 98 模式启动,所有的扩展都被禁用。如果启动时指定了它,那么所有的扩展就会被启用。如果你从网上下载了什么其它代码,但是在加载的时候出了麻烦,那么请确认你的 "--fglasgow-exts" 这个开关是否设置正确。

    (译者注: Hal Daume III 只介绍了这一个选项,哪位朋友有兴趣将其余的选项再翻译
一下,可以和我联系)

    关于 GHC 选项的进一步的信息你可以参考它的手册:

第 2.2.6 小节   如何获得帮助

    想获得关于 GHC 的帮助,请访问 GHC 的主页。想获得关于 Haskell 的帮助,请访问
Haskell 的主页。

第 2.3 节   NHC

    (有关 NHC 的内容原作者并未提到)

第 2.3.1 小节   从何获取 NHC

第 2.3.2 小节   安装 NHC

第 2.3.3 小节   如何运行 NHC

第 2.3.4 小节   程序选项

第 2.3.5 小节   如何获得帮助

第 2.4 节   程序文本编辑器

    一个好的文本编辑器可以让编程变得更加有趣。当然了,你可以用任何编辑器来编辑你
的程序,哪怕是最简单的那种编辑器也行。例如 Windows 的记事本。但是,一个好的编辑
器可以为你提供很多方面,让你专注于编程本身。

    作为 Haskell 编程来讲,一个好的程序文本编辑器应该有以下几个特点:

    ◆ 源代码语法着色,或者在单色显示器下可以高亮显示不同的语法成分。
    ◆ 自动缩进
    ◆ 可以和 Haskell 解释器( Hugs 或者 GHCi )协作。
    ◆ 代码导航
    ◆ 代码输入时的“自动完成”功能。

    截止到本文写作时,有以下几个选择: Emacs/XEmacs(需要安装 Haskell 插件)、
Vim 等等。

                                第三章 语言基础

    在本章我们会先阐述几个 Haskell 基本概念。此外,帮助你熟悉 Haskell 解释器环境
并且演示如何去编译一个简单的程序。如果你以前学过 C 或者 Java 的话,你会发现我们
将要介绍的这些语法看起来很另类。

    不管怎样,在我们开始学习 Haskell 之前,我们需要明确 Haskell 的几个基本特点。
首先,Haskell 是一种惰性语言,之所以说它是“惰性的”,是由于除非迫不得已,
否则它不对计算结果没有帮助的表达式进行求值。举个例子来说,你可以定义一个无限大的
数据结构,只要你只使用其中一部分数据而不使用全部数据,这就没有问题。再举个例子,
你可以象下面这样创建一个无限大的列表:

  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
  ┃ List makeList()                                                      ┃
  ┃ {                                                                    ┃
  ┃     List current = new List();                                       ┃
  ┃     current.value = 1;                                               ┃
  ┃     current.next = makeList();                                       ┃
  ┃     return current;                                                  ┃
  ┃ }                                                                    ┃
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

    让我们来看看这段代码:它首先创建了一个新的列表,然后设置它的节点值为 1,
然后,递归地调用它自己再创建其余的节点。很显然,这种写法在 C 或者 Java 语言中是
绝对不允许的,因为它将引起一个无限地递归过程。乍一看,这个程序一旦运行,那么将不
会结束,为什么会这样呢?这是因为我们总是下意识的认为程序设计语言是“勤快”的,而
不是“惰性”的。“勤快”的语言总是认为函数调用就意味着需要求它的值,我们称之为
“值调用”(call by value),而“惰性”的语言却是“名字调用”(call by name),换句
话说,先不需要求值,除非“迫不得已”。

    这段代码翻译成等效的 Haskell 语法应该这样写:

  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
  ┃ makeList = 1 : makeList                                              ┃
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

    这段程序的含义是:我们定义一个函数,名字叫 makeList(和其它语言不同,= 在这
里表示函数定义,而不是赋值),在等号的右边,我们给出了 makeList 的定义。
    在 Haskell 中,冒号用来创建列表(我们很快就要学习它)。等号右边的意思是指,
我们把 1 这个元素放到列表的最前面,而列表的其余部分要用 makeList 来创建。

    因为这样,Haskell 被称作是“惰性”的。上面这段代码在这个时候实际上不需要被求
值,我们只需要简单地记住:如果我们需要 makeList 的第二个元素,那么我们只需要看看
makeList 就可以了。

    现在,如果你把上面的这段代码写到一个文件中,然后用 Hugs 来运行它,打印一下它
的内容,或者计算一下它的元素个数,那么你会发现你陷入了一个死循环,因为你操作了一
个无限长的列表。

    然而,如果你只是想使用这个列表的一个有限的部分的话,比如说只需要头 10 个元
素,那么这个列表是不是无限长就没有关系。如果你只需要头 10 个元素的话,那么
Haskell 只计算头 10 个元素,这就是为什么把 Haskell 称作是惰性语言的原因。

    其次,Haskell 是大小写敏感的。实际上大多数语言都是这样,不过 Haskell 更加特
别一些。Haskell 通过大小写来区分类型(types)和值(values),例如像 1,2,3 这样的数字
值、"abc", "hello" 这样的字符串值,还有像 'a', 'b', 'c' 这样的字符值,甚至象平方
函数、平方根函数这样的函数值。Haskell 用大写字母打头的标识符来表示类型(types),
而用小写字母打头的标识符来表示值(values)。这一点和其它大多数语言不同。这意味着,
如果你不这样做将导致你的程序无法编译,所以请确信你没有用类似 "Foo" 这样的名字来
命名你的函数。函数不能以大写字母打头。

    作为一个函式语言,Haskell 会避开“副作用”。所谓“副作用”从本质上来讲,是指
一个函数在执行的过程当中产生了一些和它的输出产物无关的输出,从而影响到了它所处的
环境。

    举个例子,在 C 或者 Java 语言中,你可以在函数内部修改一个“全局变量”,这就
是一个副作用。因为它和函数的输出没有关系。然而,现实世界中副作用是不可避免的:你
也许本来就需要望屏幕上输出一些内容,或者从一个文件中读取一些内容,等等。

    没有产生任何副作用的函数就称为“纯粹的”。判断一个函数是不是“纯粹”的一个简
单的办法就是问你自己一个问题:“如果我提供同样的参数,那么这个函数能否产生同样的
输出?”

    以上这些无非是想说,如果你曾经使用命令式语言(C, Java, etc)编过程序,那么你将
开始接受一种全新的思维模式。举个例子:如果你有一个值 x,那么你不能把 x 想象成是
一个寄存器、或者变量、或者一段内存等等。x 就是一个简单的名字,就好像 "Hal" 就是
我的名字一样。你不能随心所欲地把别的什么名字当作是我的名字,你也不能随便拿来别的
一个什么值存储到 x 当中。这就是说,用 Haskell 的眼光来看,下面的这段 C 代码就是
无效的:

  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
  ┃  int x = 5;                                                          ┃
  ┃  x = x + 1;                                                          ┃
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

    类似于 x = x + 1 这样的调用被称作是“破坏式更新”(destructive update),因为
我们必须销毁 x 之前的值并且替换成一个新的值。在 Haskell 语言中“破坏式更新”是不
允许的。

    因为不允许“破坏式更新”或者其它含有副作用的操作,所以 Haskell 程序非常容易
理解。具体来讲,如果我们定义一个函数 f,并且在程序开始的地方用一个具体的参数 a 
去调用 f,然后,在程序的末尾处再用 a 去调用 f,两次得出的结果肯定是相同的。这是
因为我们知道对于用 a 来调用 f 来讲,不会有任何改变。比如,它不会递增一个全局计数
器。这种特性我们把它叫做“引用透明性”(referential transparency),就是说,如果有
两个函数 f 和 g 接受同样的参数会得出同样的结果的话,我们就可以把 f 和 g 互换,反
之亦然。

  ┌───────────────────────────────────┐
  │【注意】There is no agreed-upon exact definition of referential       │
  │        transparency. The definition given above is the one I like    │
  │        best. They all carry the same interpretation; the differences │
  │        lie in how they are formalized.                               │
  └───────────────────────────────────┘

第 3.1 节   算术运算

    译者注:阅读本节时最好先安装好 Hugs 或者 GHC,然后对照着本教材一步一步做。

    让我们先初步尝试一下如何在 Haskell 进行简单的算术运算。打开你喜欢的 Haskell
解释器环境(Hugs 或者 GHCi,参见第二章有关它们的介绍),它们会望屏幕上输出一些关
于它自己的介绍信息,然后会在光标前面出现下面这样一个提示符:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> _                                                           ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    现在,你就可以计算一些表达式了。所谓表达式就是可以表示一个值的一个东西。
例如,数字 5 就是一个表达式(它的值是 5)。值可以由其它的值组成,例如 5+6 就是一
个表达式(它的值是 11)。事实上,大多数简单的算术运算符都可以在 Haskell 中使用,
例如加法(+)、减法(-)、乘法(*)、除法(/)、乘方(^)、还有开方(sqrt)。你可以用它们来
做一些实验,让解释器来计算表达式的值。这样看来,Haskell 的解释器也可以当作一个功
能强大的计算器来用。

    先试试这几个比较简单的表达式:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> 5*4+3                                                       ┇
  ┇ 23                                                                   ┇
  ┇ Prelude> 5^5-2                                                       ┇
  ┇ 3123                                                                 ┇
  ┇ Prelude> sqrt 2                                                      ┇
  ┇ 1.4142135623730951                                                   ┇
  ┇ Prelude> sqrt(2)                                                     ┇
  ┇ 1.4142135623730951                                                   ┇
  ┇ Prelude> 5*(4+3)                                                     ┇
  ┇ 35                                                                   ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    我们可以看到,除了加减乘除以外,Haskell 还允许使用括号来改变运算的优先级。这
一点可以从 5*4+3 与 5*(4+3) 的区别上看出来。

    我们也可以看到,函数的参数两旁的括号并不是必须的。sqrt 2 和 sqrt(2) 并没有什
么区别。括号在大多数其它语言中是必须的,而 Haskell 里边,并不要求这样做。

  ┌┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┐
  ┊【警告】尽管圆括号并不是必须的,但是书写的时候最好加上,这将使你的代码┊
  ┊        更加容易被人理解。                                            ┊
  └┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┘

    现在,让我们试一下 2^5000,看看会有什么事情发生?

  ┌───────────────────────────────────┐
  │【注意】如果你比较习惯 C 或者其它语言编程的话,你也许会奇怪 sqrt 2 这 │
  │        种写法为什么传递进去的是一个整型数,而得到的却是一个小数?这种│
  │        数值类型之间的可变换性由 Haskell 的类型系统来实现。详细内容会 │
  │        在后面第 4.3 节讨论。                                         │
  └───────────────────────────────────┘

                      ┏━━━━━━━━━━━━━━━━┓
                      ┃             习题               ┃
                      ┗━━━━━━━━━━━━━━━━┛

习题 3.1 We've seen that multiplication binds more tightly than division.
Can you think of a way to determine whether function application binds
more or less tightly than multiplication?

We've seen that multiplication binds more tightly than division.
Can you think of a way to determine whether function application binds more or less tightly than multiplication?

第 3.2 节   二元组、三元组和多元组

    除了单个的值之外,我们还可以使用多个值。举个例子说,当我们要提及一个点的两个
坐标值的时候,我们需要一个整数对来表示 x/y 坐标。表示一个整数对的简单方法是:用
一个逗号把它们俩分开,然后用一对圆括号括起来。就像下面这样:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> (5,3)                                                       ┇
  ┇ (5,3)                                                                ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    现在我们知道如何表示 5 和 3 这一个“整数对”了。在 Haskell 中,一个“值对”
(以后我们称作是“二元组”)的第一个元素和第二个元素的类型不要求相同。这就是说,
你可以创建各种各样的二元组。例如,你可以写一个由整数和字符串组成的二元组。和列表
相比较,列表则需要每个元素的类型都相同(关于列表我们在第 3.3 节讨论)。

    Haskell 有两个预定义函数用来操作二元组。它们是 fst 和 snd,分别用来取出二元
组的第一个元素和第二个元素。下面看看它们是如何使用的:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> fst (5, "hello")                                            ┇
  ┇ 5                                                                    ┇
  ┇ Prelude> snd (5, "hello")                                            ┇
  ┇ "hello"                                                              ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    除了二元组,我们还可以定义三元组(triples),或者四元组(quadruples)……等等。
下面我们试着定义一个三元组和一个四元组:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> (1,2,3)                                                     ┇
  ┇ (1,2,3)                                                              ┇
  ┇ Prelude> (1,2,3,4)                                                   ┇
  ┇ (1,2,3,4)                                                            ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    一般来讲,二元组、三元组、四元组等等它们统称为“元组”,并且可以存储固定数
量的各种各样的数据。

  ┌───────────────────────────────────┐
  │【注意】函数 fst 和 snd 只能用于二元组,不能用在三元组或者多元组上面,│
  │        否则你会看到一个指示“类型错误”的错误信息。我们会在第四章给  │
  │        出这个错误信息的解释。                                        │
  └───────────────────────────────────┘

                      ┏━━━━━━━━━━━━━━━━┓                      
                      ┃             习题               ┃
                      ┗━━━━━━━━━━━━━━━━┛

习题 3.2 请使用 fst 和 snd 函数,取出后面这个元组的各个元素:((1,'a'),"foo")

第 3.3 节   列表

    元组最大的局限性在于它只能拥有固定数目的元素:二元组只能有两个元素,三元组只
能有三个元素,四元组只能有四个元素,等等。能够拥有随意数目的元素的数据结构叫做
“列表”。列表是一个和元组非常类似的集合,唯一不同的地方是它用方括号来替代元组中
的圆括号。我们可以这样定义一个列表:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> [1,2]                                                       ┇
  ┇ [1,2]                                                                ┇
  ┇ Prelude> [1,2,3]                                                     ┇
  ┇ [1,2,3]                                                              ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    列表可以不包括任何元素,这样的列表叫做“空列表”,用符号 [] 表示。和元组不同
的是,我们可以很容易地用冒号操作符往列表的开始处添加元素。在英文中,冒号操作符也
被称作是“cons”操作符,往一个列表中添加元素的操作则称作是“consing”。请各位读
者朋友们在阅读英文资料时注意。从词法角度将,用冒号操作符来向列表添加元素实际上是
使用一个元素和一个旧的列表来构造一个新的列表。下面的例子用来演示冒号操作符:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> 0:[1,2]                                                     ┇
  ┇ [0,1,2]                                                              ┇
  ┇ Prelude> 5:[1,2,3,4]                                                 ┇
  ┇ [5,1,2,3,4]                                                          ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    这样,我们可以认为所有的列表都是在空列表 [] 的基础上不断地施加冒号操作符运算
来得到的:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> 5:1:2:3:4:[]                                                ┇
  ┇ [5,1,2,3,4]                                                          ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    事实上,Haskell 编译器正是这么处理的。当我们书写 "[5,1,2,3,4]" 这样的表达式
时,编译器实际上只是简单地把它转换成 "5:1:2:3:4:[]" 来处理。

  ┌───────────────────────────────────┐
  │【注意】对于 Haskell 这种语言来讲,[5,1,2,3,4] 这种写法实际上是一种不 │
  │        必要的写法。它唯一存在的意义在于使源代码看上去好看一些。      │
  └───────────────────────────────────┘

    再进一步来讲,列表和元组的不同点在于,元组的各个元素可以是各式各样的,而列表
则必须由是同一种类型的元素构成。这就是说,你不能构造一个同时包含有整数和字符串的
列表,如果你试图那么做,Haskell 将会报告一个类型错误。

    当然,列表并非只能包含整型元素和字符串元素。它可以包含元组甚至是其它的列表。
同样地,元组也可以包含其它的列表或者元组。让我们试一下下面的语句:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> [(1,1),(2,4),(3,9),(4,16)]                                  ┇
  ┇ [(1,1),(2,4),(3,9),(4,16)]                                           ┇
  ┇ Prelude> ([1,2,3,4],[5,6,7])                                         ┇
  ┇ ([1,2,3,4],[5,6,7])                                                  ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    针对列表来讲,有两个基本操作:head 和 tail。head 返回非空列表的第一个元素,
tail 返回非空列表去除第一个元素的剩下的部分。译者注:如果对空列表 [] 进行 head
或者 tail 操作,那么将报告一个错误信息。

    如果想要获得列表的长度,可以使用 length 函数。

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> length [1,2,3,4,10]                                         ┇
  ┇ 5                                                                    ┇
  ┇ Prelude> head [1,2,3,4,10]                                           ┇
  ┇ 1                                                                    ┇
  ┇ Prelude> length (tail [1,2,3,4,10])                                  ┇
  ┇ 4                                                                    ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

第 3.3.1 小节   字符串

    在 Haskell 中,字符串实际上就是一个字符型的简单列表。你可以像下面这样子来创
建一个字符串:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> 'H':'e':'l':'l':'o':[]                                      ┇
  ┇ "Hello"                                                              ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    所有的列表都可以使用 ++ 运算,++ 可以链接两个列表,因为字符串也是列表,所以
字符串也可以这么做:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> "Hello " ++ "World"                                         ┇
  ┇ "Hello World"                                                        ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    此外,非字符串值可以被转换成字符串值以便于调用 show 函数来显示它。反过来,字
符串也可以用 read 函数转换成非字符串值。当然,如果你尝试 read 一个畸形的值,那么
会出错(注意,这是一个运行时错误,而不是编译时错误)。

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> "Five squared is " ++ show (5*5)                            ┇
  ┇ "Five squared is 25"                                                 ┇
  ┇ Prelude> read "5" + 3                                                ┇
  ┇ 8                                                                    ┇
  ┇ Prelude> read "Hello" + 3                                            ┇
  ┇ Program error: Prelude.read: no parse                                ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    正如上面看到的一样,错误信息的准确程度依耐于工具软件。不管怎样,解释器推断出
你想要把 3 加到别的东西上面。这就是说,编译器推断出我们期望 read "Hello" 能够返
回一个数字,而事实上却不能处理成一个数字,因此就会产生 "no parse" 的错误。

第 3.3.2 小节   简单列表函数

    Haskell 程序中有大量的列表处理计算。主要有三个列表处理函数:map, filter 和
foldr(foldl)。

    map 函数有两个参数,第一个参数是一个函数 f,第二个参数是一个列表 l。map 函数
可以将 f 运算施加到参数 l 中的每一个元素上。并且返回施加过 f 运算的列表 l。举个
例子,有一个内建函数叫做 Char.toUpper,它接受一个小写字母,并且返回相应的大写字
母。现在,让我们试着用 map 将 Char.toUpper 这个函数施加到一个字符串上面:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> map Char.toUpper "Hello World"                              ┇
  ┇ "HELLO WORLD"                                                        ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    用 map 函数处理列表时,列表的长度不会产生任何变化,变化的只是每个列表的元素
值。

    如果要从列表中删除元素,那么你可以使用 filter 函数。这个函数允许你从列表中删
除符合条件的元素。被删除的元素只和它的值有关,而和它所处的上下文位置无关。举个例
子,函数 Char.isLower 可以告诉你一个字母是不是小写的。因此,我们可以用下面的语句
过滤掉所有不是小写字母的字符,而只留下小写字母:(删除了 H W 和空格)

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> filter Char.isLower "Hello World"                           ┇
  ┇ "elloorld"                                                           ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    foldr 是个很神奇的函数,它一共有三个参数:一个函数、一个初始值、一个列表。理
解 foldr 函数的一个好办法是,你先把第三个参数提供的列表书写成用冒号分割每个元素
的格式,然后你假设 foldr 可以用参数中的函数替换掉列表中的冒号运算符,并且用参数
中的初始值替换掉 [],然后就好了。打个比方,比如你有这样的一个列表:

    3 : 8 : 12 : 5 : []

    当我们对它施加 foldr (+) 0 运算时,相当于做了这样一件事:

    3 + 8 + 12 + 5 + 0

    在解释器中再试一下:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> foldr (+) 0 [3,8,12,5]                                      ┇
  ┇ 28                                                                   ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    再试一下下面这个式子:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> foldr (*) 1 [4,8,5]                                         ┇
  ┇ 160                                                                  ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    因为加法 (+) 和乘法 (*) 都是符合结合律的,所以上面两个比较好理解,下面这个就
不那么好理解了:

  ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
  ┇ Prelude> foldr (-) 1 [4,8,5]                                         ┇
  ┇ 0                                                                    ┇
  ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

    为什么呢?上面这个式子到底表示的是 ((4-8)-5)-1 还是 4-(8-(5-1)) 呢?到底是右
边的先结合,还是左边的先结合呢?答案是右边的先结合。也就是说,计算过程实际上类似
于下面的过程:

    foldr (-) 1 [4,8,5]
> 4 - (foldr (-) 1 [8,5])
> 4 – (8 – (foldr () 1 [5]))
> 4 - (8 - (5 - (foldr (-) 1 []))) > 4 – (8 – (5 – 1))
> 4 - (8 - 4) > 4 – 4
> 0 foldl 函数和 foldr 函数在处理满足结合律的运算时,表现是一样的: ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓ ┇ Prelude> foldl (+) 0 [3,8,12,5] ┇ ┇ 28 ┇ ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛ 但是在处理不满足结合律的运算时,却大有分别: ┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓ ┇ Prelude> foldl (-) 1 [4,8,5] ┇ ┇ -16 ┇ ┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛ 它的演算过程是这样的: foldl (-) 1 [4,8,5] > foldl (
) (1 – 4) [8,5]
> foldl (-) ((1 - 4) - 8) [5] > foldl (-) (((1 – 4) – 8) – 5) []
> ((1 - 4) - 8) - 5 > ((-3) – 8) – 5
> (-11) - 5 > -16

译者注:我发现了 foldl (-) 的另一种推算方式,这两种推算方式其实是等效的,所以你
在记忆的时候,可以有选择地记住你认为方便记忆的其中一种。

foldl () 1 [4,8,5]
> foldl (-) 1 [4,8] - 5 > (foldl (
) 1 [4] 8) 5
> ((foldl (-) 1 []) - 4) - 8) - 5 > ((1 – 4) – 8) – 5
> ((-3) - 8) - 5 > (-11) – 5
> -16
注意 foldl 的运算结合顺序正好和 foldr 是相反的。就连初始值也是放在左边,而不
是右边。
┌───────────────────────────────────┐
│【注意】foldl 通常要比 foldr 更有效率,关于这一点我们在第 7.8 节讨论。│
│        不过,foldr 可以操作无限列表,而 foldl 却不能,这是因为 foldl │
│        必须要等到处理完整个列表才能开始计算,而 foldr 却可以边计算边 │
│        处理。举个例子来说,foldr (:) [] [1,2,3,4,5] 简单的返回同样的 │
│        一个列表,甚至是无限的列表它也是如此,它可以产生输出。但是如果│
│        让 foldl 来处理,它就永远不会返回,因为它永远处理不完一个无限 │
│        列表,处理不完也就无法开始计算,因此它不会有结果。最终它只能由│
│        于堆栈溢出而终止执行。                                        │
└───────────────────────────────────┘
译者注:为了方便初学者理解上面这段话,我特意构造了一个无限列表,请读者将下面
这段代码输入到你的编辑器中,并且把它单独保存到你的工作目录中且命名为 ttt.hs:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ from n = n : from(n+1)                                               ┃
┃ from1 = from 1                                                       ┃
┃ isquote ch = ch /= '\+ + (show y) )             ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
然后在你的工作目录中启动你的 Haskell 解释器(Hugs or GHCi),并且在解释器中输
入如下命令:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ :load ttt.hs                                                         ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
然后,再分别执行 foldr (f) "" from1 和 foldl (f) "" from1 试试,你会发现前者
能够不断地输出用冒号分割的自然数,而后者却不能输出。这是因为 foldr 调用虽然无法
结束,但它确实可以一直运行,而 foldl 则因为永远无法开始计算,从而不能输出任何结
果。
我再简单地介绍一下刚才那个程序的含义,第一行用来产生一个无限列表的定义,第二
行用来产生一个从 1 开始的所有自然数的无限数列。第三行定义了一个函数,用来判断一
个字符是不是双引号,这个函数在第四行中被作为过滤器使用。第四行定义了一个函数,
它可以将传递给它的两个参数用冒号连接起来,并且用 filter 过滤掉所有的引号。
如果以上针对 foldl 和 foldr 函数的讨论仍然有不清楚的地方,那也不要紧。我们会
在第 7.8 节继续作进一步的讨论。
习题

习题 3.3 请使用 map 函数用一个字符串构造出一个包含有布尔值的列表。构造出的列表 中的每个元素用来表示前面的字符串的每个字符是不是小写字母。例如,对于 “aBCde” 应该生成一个 [True,False,False,True,True] 这样的列表。

习题 3.4 请使用本节介绍的函数想办法计算出一个字符串中一共有几个小写字母。比如, “aBCde” 应该计算出 3。你可能需要几个函数一起配合使用。

习题 3.5 我们已经看到 foldr/foldl 的用法了。现在我们再提供一个函数 max 给你,它 可以通过比较返回两个数当中较大的一个。现在请你想办法用 foldl/foldr 和 max 来构造一个式子,可以返回一个数值列表中的最大数(如果列表是空的就 返回 0,并且假设这个列表中的数字都大于 0)。打个比方,[5,10,2,8,1] 应 该计算出 10。并且解释一下你的思路。

习题 3.6 假设有一个列表,它由至少两个二元组构成,请写一个式子,可以取出列表中第 二个二元组的第一个零件。比如给出 [ (5,’b’), (1,’c’), (6,’a’) ] 就应该 可以得到 1。

译者注:原文中上面几个题目都要求“写出一个函数”,但是考虑到读者此时尚未学习函数
的写法,所以译文中改为要求“写出一个表达式”。

第 3.4 节 源代码文件

作为一个程序员,我们不可能总是像在上面那样交互式地输入一些表达式来计算,我们
可能更加需要坐下来用自己喜爱的编辑器编写一段代码,然后保存成文件再来重复使用它。
在第 2.2 节和 2.3 节中,我们已经看到如何书写一个 "Hello World" 程序并且如何
编译、运行它了。现在,我们将看到如何在源代码中定义一个函数,并且在解释环境中运行
它。首先,我们创建一个名为 Test.hs 的文件并且在其中输入如下代码:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ module Test                                                          ┃
┃    where                                                             ┃
┃                                                                      ┃
┃ x = 5                                                                ┃
┃ y = (6, "Hello")                                                     ┃
┃ z = x  fst y                                                        ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
这是一个非常简单的 Haskell 程序。它定义了一个名为 Test 的模块(通常模块名和
文件名应该相同,请参考第 6 章的内容)。在这个模块中,一共有三个定义 x, y 和 z。
一旦你写好了这个文件,并且放在了你的工作目录,那么你就可以用你喜欢的解释器来加载
它:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ % hugs Test.hs                                                       ┇
┇ % ghci Test.hs                                                       ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
如果你已经启动了解释器,那么你可以用 load 命令来加载它,就像这样:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Prelude> :load Test.hs                                               ┇
┇ ...                                                                  ┇
┇ Test>                                                                ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
省略号出表示解释器可能会输出一些不同的内容。一旦提示符改变为你的模块名字,
那么就表示你的模块已经成功加载。如果出现了一些错误,那么请重新编辑你的文件,检查
是不是和本文中给出的一样。
提示符 "Test>" 表示当前模块是 Test 模块。那么你可能会猜测到,Prelude 是不是
也是一个模块呢?一点儿都没错!Prelude 正是一个系统自带的模块,它在解释器一启动的
时候就自动加载,其中包含了诸如加减乘除、冒号、开方、fst、snd 等函数的定义。
现在,Test 模块已经成功加载,那么我们就可以使用我们在其中定义的一些函数了,
请看例子:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Test> x                                                              ┇
┇ 5                                                                    ┇
┇ Test> y                                                              ┇
┇ (6,"Hello")                                                          ┇
┇ Test> z                                                              ┇
┇ 30                                                                   ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
非常好!正是我们所期望的。
最后还有个问题,那就是我们如何去编译它,使之成为一个标准的可执行文件。关于这
一点 Haskell 有个规定,就是说源文件中必须得有一个名为 "Main" 的模块,并且 Main
模块中必须得有一个名为 "main" 的函数。因此,我们得修改一下 Test.hs 文件,将其中
的模块名称从 Test 改成 Main。并且再增加一个 main 函数,修改后的完整代码如下:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ module Main                                                          ┃
┃     where                                                            ┃
┃                                                                      ┃
┃ x = 5                                                                ┃
┃ y = (6, "Hello")                                                     ┃
┃ z = x  fst y                                                        ┃
┃ main = putStrLn "Hello World"                                        ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
现在,保存它,然后来编译。你可以用 NHC 或者 GHC 来编译,本书中一律以 GHC 为
例。用下面的命令就可以编译:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ % ghc --make Test.hs -o test                                         ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
这样,将会生成一个名为 test 的文件。Windows 环境将会生成一个名为 test.exe 的
文件。如果是 Unix/Linux 用户,那么你还可能需要用 chmod 命令使之可以执行:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ % chmod 755 test                                                     ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
然后我们就可以运行它了:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ % ./test                                                             ┇
┇ Hello World                                                          ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
Windows 用户可以这么做:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ C:\> test.exe                                                        ┇
┇ Hello World                                                          ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

第 3.5 节 函数

现在,我们已经知道如何在文件中书写代码了,下面我们开始学习如何书写函数。正如
你所预料的一样,函数是 Haskell 语言的核心,这也是 Haskell 语言为什么被称作是“函
数型编程语言”的原因,这意味着,程序的求值就和函数的求值是一样的。
我们可以在我们的 Test.hs 文件中自己定义一个平方函数,请看下面:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ square x = x  x                                                     ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
在这里例子中,我们定义了一个名为 square 的函数,它有一个名为 x 的参数。然后
我们就可以说“square x 的值是 x
x”。
Haskell 也支持条件表达式。例如,你可以定义这样一个函数:当它的参数小于 0 时
返回 -1,当它的参数等于 0 时返回 0,当它的参数大于 0 时返回 1(这样的函数通常被
叫做“符号函数”,意为只取一个数值的符号),请看下面:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ signum x =                                                           ┃
┃     if x < 0                                                         ┃
┃       then -1                                                        ┃
┃       else if x > 0                                                  ┃
┃         then 1                                                       ┃
┃         else 0                                                       ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Haskell 中的 if/then/else 和其它语言的非常相似。不过,你必须书写 then 和
else 两个分支,这一点和 C 语言不同。C 语言可以只有 then 分支而没有 else 分支。
if 语句首先判断条件的值,在本例中,如果 x < 0,那么会计算出 True,于是它将执
行 then 分支,否则它就执行 else 分支。你可以编辑你的 Test.hs 测试一下 signum。
如果解释器的当前模块已经是 Test 了,那么你可以简单地用 :reload 命令(或者干脆写
作 :r)来替代 :load Test.hs。
Haskell 也支持 case 语句,它可以进行多分支判断(if/then/else)只能进行双分支
判断。后面第 7.4 节我们会更加详细地讨论 case 的用法。
假设我们想要定义这样一个函数:当它的参数值是 0 时,返回 1;参数值时 1 时,返
回 5;参数值是 2 时,返回 2;其它情况一律返回 -1。这个函数如果用 if 语句来写将会
非常繁琐,而且可读性也不好。这时我们可以用 case 语句:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ f x =                                                                ┃
┃     case x of                                                        ┃
┃       0 -> 1                                                         ┃
┃       1 -> 5                                                         ┃
┃       2 -> 2                                                         ┃
┃       _ -> -1                                                        ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
在这个程序中,我们定义了函数 f,它可以携带一个参数 x,它判断 x 的值,如果是
0 的话,那么就返回 1,如果是 1 的话,那么就返回 5,如果是 2 的话,那么就返回 2,
如果什么都不是的话,就返回 -1。下划线是个通配符,用来代表所有其它情况不能满足的
时候的值。
在上面的代码中,正确的“缩进”是非常重要的。Haskell 用一个称作是“布局”的系
统来组织代码。这一点和 Python 语言中有些类似。布局系统允许你不用像在 C/Java 中那
样用显式的分号和花括号来组织代码结构。
┌┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┐
┊【警告】因为 Haskell 使用空白符来组织代码结构,所以你需要很小心地键入 ┊
┊        空格键或者 TAB 键。你最好配置一下你的编辑器,让它从来都不使用 ┊
┊        TAB 字符,这也许会好一些。否则的话,最好保持你的 TAB 始终都是 ┊
┊        8 个字符。                                                    ┊
└┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┘
布局系统在 Haskell 内部处理的时候,实际上就是在 where, let, do 和 of 这几个
关键字之后插入一个左花括号,并要求下一条语句往右缩进至少一个空格。它会记住下一行
的起始位置,如果接下来的有一行的起始位置左移超过了最初的那个起始位置了,那就意味
着一个语句块的结束,这时它会自动插入一个右花括号。并且这一对花括号之间的每个语句
末尾都插入一个分号。这看起来似乎很麻烦,其实你只要记住每次遇到 where, let, do 和
of 之后就缩进一下就可以了。在第 7.11 节我们会讨论更多的关于布局的话题。
有些人也许会不适应 Haskell 的布局系统,那么它也可以用花括号和分号来显式地注
明语句层次。如果这样的话,上面那段程序就可以这么写:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ f x = case x of                                                      ┃
┃         { 0 > 1 ; 1 > 5 ; 2 > 2 ; _ > -1 }                       ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
当然了,一旦你用花括号和分号显式地注明语句层次的话,那么完全就可以自由地组织
你的代码格式,比如像这样也是允许的:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ f x =                                                                ┃
┃     case x of { 0 -> 1 ;                                             ┃
┃       1 > 5; 2 > 2                                                 ┃
┃  ; _ -> -1 }                                                         ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
总之呢,你可以随便写,因为 Haskell 不再会在意你有没有合适的缩进,因为此时它
已经能够根据花括号和分号区分开语句之间的层次关系。
然而,无论如何,请尽量将的你代码写得整齐些。谢谢!
Haskell 中的函数定义可以采用 "piece-wise" 的形式。它的意思呢就是说,你可以为
某一个固定的参数而书写一个专门的版本,然后再为其它情况书写一个版本。举个例子说,
上面的那个 f 函数也可以这么写:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ f 0 = 1                                                              ┃
┃ f 1 = 5                                                              ┃
┃ f 2 = 2                                                              ┃
┃ f _ = -1                                                             ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
注意,这时候先后顺序显得非常重要!如果你把最后一行放到了前面,那么其余各行就
无法生效。因为 Haskell 已经认定不管传递什么参数(_)都会返回 -1。有些编译器在碰到
这种情形的时候会向你提出一个警告。如果我们不写最后一行的话,那么函数 f 当碰到除
了 0, 1, 2 之外的参数时会报告一个错误。同样,有些编译器会给你提出警告。
这种风格的写法非常流行,本书中到处都可以看到。事实上,f 函数的这两种写法(分
开定义固定参数值和使用 case 区分)是等效的。"piece-wise" 写法在内部被转换成 case
形式来处理。
大多数复杂函数实际上都能够通过简单函数“复合”来得到。这里的“复合”和数学中
的“复合”是同一个概念。复合函数只是简单地将一种一个函数的运算结果作为参数传递给
另一个函数。在本书第 3.1 节已经看到了这样的例子。当你书写 54+3 的时候,实际上是
先计算 5
4 然后把计算结果再和 3 相加就可以得出最后结果。在下面的例子中,我们将平
方函数 square 和前面定义的 f 进行“复合”:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Test> square (f 1)                                                   ┇
┇ 25                                                                   ┇
┇ Test> square (f 2)                                                   ┇
┇ 4                                                                    ┇
┇ Test> f (square 1)                                                   ┇
┇ 5                                                                    ┇
┇ Test> f (square 2)                                                   ┇
┇ -1                                                                   ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
在上面的例子中,圆括号起到了说明计算顺序的作用。如果不这样做的话,在上面的第
一行中,Haskell 可能会以为我们想要把 "f 1" 传递给 "square" 来计算。在数学中,这
种情形可以用 (。)来表示。考虑到输入问题,在 Haskell 中用小数点(.)来代替。
┌───────────────────────────────────┐
│【注意】在数学中, f。g (x) = f(g(x)),这表示,使用参数 x 来调用 f。g │
│        等效于先用参数 x 来调用 g,然后把调用结果当作参数再来调用 f   │
│        (译者注:数学中的复合运算符 "。" 实际上是一个空心的圆点,因为│
│        不好输入,所以用句号代替)                                    │
└───────────────────────────────────┘
在 Haskell 中,(.) 实际上也是一个函数,称作“函数复合函数”,意思是说,这个
函数可以将两个函数进行复合,其结果就是复合后的“复合函数”。举个例子来说,如果我
们写 (square . f),那就表示这将产生一个新的函数,它会先将参数传递给 f 去计算,然
后把计算的结果再传递给 square 来计算,计算后产生的结果才是最后结果。反过来讲,复
合函数 (f . square) 的意思就是说创建一个新的函数,它先将参数用 square 来计算,然
后把计算的结果再传递给 f 来计算以产生最后结果。下面我们看看这两个有什么不同:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Test> (square . f) 1                                                 ┇
┇ 25                                                                   ┇
┇ Test> (square . f) 2                                                 ┇
┇ 4                                                                    ┇
┇ Test> (f . square) 1                                                 ┇
┇ 5                                                                    ┇
┇ Test> (f . square) 2                                                 ┇
┇ -1                                                                   ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
注意,我们必须用圆括号把复合函数括起来。如果不这样做的话,在上面的第一行中,
Haskell 可能会以为我们想要把 "f 1" 传递给 "square" 来计算。
现在,我们有必要略微停顿一下,来看看在 Prelude 中都定义了哪些函数。不然,也
许我们一不小心就重写了已经存在的函数。请看下面:
sqrt 平方根函数 id ID 函数。 id x = x fst 返回一个二元组中的第一个元素 snd 返回一个二元组中的第二个元素 null 告诉你一个列表是不是空的 head 从一个非空列表中第一个元素 tail 从一个非空列表中返回除了去除第一个元素后的其余部分 ++ 连接两个列表 比较两个元素是不是相等 /= 比较两个元素是不是不相等
下面我们演练一下:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Prelude> sqrt 2                                                      ┇
┇ 1.41421                                                              ┇
┇ Prelude> id "hello"                                                  ┇
┇ "hello"                                                              ┇
┇ Prelude> id 5                                                        ┇
┇ 5                                                                    ┇
┇ Prelude> fst (5,2)                                                   ┇
┇ 5                                                                    ┇
┇ Prelude> snd (5,2)                                                   ┇
┇ 2                                                                    ┇
┇ Prelude> null []                                                     ┇
┇ True                                                                 ┇
┇ Prelude> null [1,2,3,4]                                              ┇
┇ False                                                                ┇
┇ Prelude> head [1,2,3,4]                                              ┇
┇ 1                                                                    ┇
┇ Prelude> tail [1,2,3,4]                                              ┇
┇ [2,3,4]                                                              ┇
┇ Prelude> [1,2,3] ++ [4,5,6]                                          ┇
┇ [1,2,3,4,5,6]                                                        ┇
┇ Prelude> [1,2,3] == [1,2,3]                                          ┇
┇ True                                                                 ┇
┇ Prelude> 'a' /= 'b'                                                  ┇
┇ True                                                                 ┇
┇ Prelude> head []                                                     ┇
┇                                                                      ┇
┇ Program error: {head []}                                             ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
我们已经看到将 head 施加于一个空列表将产生一个错误--错误信息可能和你所使用
的环境有关。这里给出的是 Hugs 显示的错误。

第 3.5.1 小节 Let 绑定

我们尝尝希望能在我们的函数内部进行一个局域范围内的定义。譬如说,如果你还记得
读中学时数学课上学过的一元二次方程的求根公式的话,那么我们可以用下面的函数获得任
意一个一元二次方程 ax2 bx c = 0 的两个根:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ roots a b c =                                                        ┃
┃     ( ( b + sqrt(bb  4ac) ) / (2a),                            ┃
┃       ( b  sqrt(bb - 4ac) ) / (2a) )                           ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
在上面的例子中, sqrt(bb  4ac) 这个繁琐的式子我们需要书写两遍。也许你还
记得在数学中我们把它叫做 Δ(读做 delta),它可以用来判断一个一元二次方程是否有
实数根,如果在我们的 roots 函数中还需要再判断一次它的话,那还得再写一遍,是不是
感觉很糟糕?因此,为了解决这个问题,Haskell 允许定义一个“局域绑定”。这样,我们
就可以在函数内部创建一个只有该函数自己才能看到的“局域绑定”。例如,在这个例子中
我们可以把 sqrt(b
b 4ac) 定义成一个局域绑定,好比就叫做 "det",然后我们就可
以把所有出现 sqrt(bb - 4ac) 的地方都用 "det" 来代替,请看下面:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ roots a b c =                                                        ┃
┃     let det = sqrt(bb - 4ac)                                      ┃
┃     in ( (-b + det) / (2a),                                         ┃
┃          (b  det) / (2a) )                                        ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
如上所示,let/in 语句用来进行局域绑定。在一个 let 语句中,可以同时定义多个绑
定,请看下面:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ roots a b c =                                                        ┃
┃     let det = sqrt(bb - 4ac)                                      ┃
┃         twice_a = 2a                                                ┃
┃     in ( (-b + det) / twice_a,                                       ┃
┃          (b  det) / twice_a )                                      ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

第 3.5.2 小节 中缀函数

中缀函数是指那些由符号而不是字母组成的函数。例如 () (-) () (+) 等都是中缀
函数。你也可以使用它们的“非中缀”形式。请看下面的两个例子:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Prelude> 5 + 10                                                      ┇
┇ 15                                                                   ┇
┇ Prelude> (+) 5 10                                                    ┇
┇ 15                                                                   ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
反过来,非中缀形式的函数也可以通过附加反引号 `` 来使用它们的中缀形式:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Prelude> map Char.toUpper "Hello World"                              ┇
┇ "HELLO WORLD"                                                        ┇
┇ Prelude> Char.toUpper `map` "Hello World"                            ┇
┇ "HELLO WORLD"                                                        ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛

第 3.6 节 注释

Haskell 中有两种注释:行注释和块注释。行注释是从 - (两个减号)开始直到行尾
为止。块注释则用 {
和 -} 括起来。和 C 语言不同,Haskell 的块注释可以嵌套。
┌───────────────────────────────────┐
│【注意】Haskell 中的 - 类似于 C++ 中的 //,Haskell 中的 { -} 类似于 │
│        C++ 中的 / /。                                              │
└───────────────────────────────────┘
注释使得你可以用自然语言来解释你的程序,编译器或者解释器会忽略所有的注释。
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ module Test2                                                         ┃
┃     where                                                            ┃
┃ main =                                                               ┃
┃     putStrLn "Hello World" -- write a string                         ┃
┃                            -- to the screen                          ┃
┃                                                                      ┃
┃ {- f is a function which takes an integer and                        ┃
┃ produces integer. {- this is an embedded                             ┃
┃ comment -} the original comment extends to the                       ┃
┃ matching end-comment token: -}                                       ┃
┃ f x =                                                                ┃
┃     case x of                                                        ┃
┃       0 > 1            - 0 maps to 1                               ┃
┃       1 > 5            - 1 maps to 5                               ┃
┃       2 > 2            - 2 maps to 2                               ┃
┃       _ > -1           - everything else maps to -1                ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
上面这个例子程序同时演示了行注释、块注释、嵌入式注释。

第 3.7 节 递归

在命令式语言如 C 或者 Java 中,程序的基本结构是循环。然而,循环在 Haskell 中
毫无意义:因为它需要不断地“破坏式更新”循环变量。作为替代,在 Haskell 中则大量
使用“递归”。
如果一个函数重复地调用自己,则称该函数是“递归函数”(参见附录 B)。递归函数
在 C 和 Java 也有,但是相比起函数型编程语言来讲不怎么使用。递归函数的一个典型例
子就是“阶乘”函数。在命令式语言中,阶乘函数可以这么写(以 C/C++ 为例):
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ int factorial( int n ) {                                             ┃
┃     int fact = 1;                                                    ┃
┃     for ( int i = 2; i <= n; i++ )                                   ┃
┃         fact = fact  i;                                             ┃
┃     return fact;                                                     ┃
┃ }                                                                    ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
上面用的是循环方式,如果采用递归形式,那么就可以这么写:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ int factorial( int n ) {                                             ┃
┃     if ( n == 1 )                                                    ┃
┃         return 1;                                                    ┃
┃     else                                                             ┃
┃         return n  factorial(n-1);                                   ┃
┃ }                                                                    ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
我们把它转换成 Haskell 代码:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ factorial 1 = 1                                                      ┃
┃ factorial n = n  factorial (n-1)                                    ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
掌握递归是一件很困难的事。它完全类似于数学中的“归纳”概念(更多严谨的介绍请
参见附录 B)。我们这里要说的是,通常一个问题可以划分成一个和多个基本情况以及一个
或多个递归情况。在计算阶乘的这个例子中,有一个基本情况(当 n=1 时)和一个递归情
况(当 n>1 时)。当你设计你的算法时,一般要按这两种情况来分别处理。
接下来我们再考虑一下求乘方(取幂)的问题,假设我们有两个正整数 a 和 b,那么
我们如何计算 a 的 b 次方?按照刚才讲过的思路,我们把这个问题分成两种情况:b=1 时
和 b>1 时:
/  a              当 b=1 时
ab = | 
       \ a  a^(b-1)     当 b>1 时
现在我们把它写成 Haskell 程序:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ exponent a 1 = a                                                     ┃
┃ exponent a b = a  exponent a (b-1)                                  ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
这就好了。
实际上,我们不仅可以对整数进行递归运算,对列表也可以进行递归运算。针对列表设
计递归算法时,通常把空列表 [] 当作基本情况(递归终止条件),而把非空列表当作递归
情况。
下面考虑如何计算一个列表的长度。我们可以将计算列表长度分成两种情况:空列表和
非空列表。空列表的长度等于 0,而非空列表的长度等于摘掉列表的第一个元素之后剩下的
部分的长度加 1。按照这个思路,我们可以这么定义计算列表长度的函数:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ my_length [] = 0                                                     ┃
┃ my_length (x:xs) = 1 + my_length xs                                  ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┌───────────────────────────────────┐
│【注意】因为 Haskell 中已经有一个标准函数叫做 length,所以我们在前面  │
│        加一个 my_ 前缀,这样的话编译器就不会搞混。以后在编写类似的函 │
│        数时都应该这么做。                                            │
└───────────────────────────────────┘
同样地,我们可以试着编一下 filter 函数。同样地,基本情况是空列表,递归情况是
非空列表。请看程序:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ my_filter p [] = []                                                  ┃
┃ my_filter p (x:xs) =                                                 ┃
┃     if p x                                                           ┃
┃        then x : my_filter p xs                                       ┃
┃        else my_filter p xs                                           ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
在这个例子中,当传递给 my_filter 一个空列表时,只是简单地返回一个空列表。当
传递给 my_filter 一个非空列表时(我们用 x:xs 表示),我们需要判断 x 是不是应该被
过滤掉,这个判断用 p x 来完成,在 filter 的原型中,p 是一个返回 Bool 型的函数。
如果 p x 返回 Ture,那么就说明 x 需要保留,这时候我们把 x 加入返回的列表的前面,
然后递归地处理剩下的部分。如果 p x 返回 False,说明 x 需要剔除,这时我们只处理剩
下的部分。
同样地,map 和 foldl/foldr 这些函数都可以自己实现。有关 foldl/foldr 的实现参
见第 7 章。
┏━━━━━━━━━━━━━━━━┓                      
┃             习题               ┃
┗━━━━━━━━━━━━━━━━┛

习题 3.7 “菲波那契数列”是指:

/  1                     当 n = 1 或者 n = 2 时
    F (n) = |
            \  F (n-2) + F (n-1)     其它情况
请写一个递归函数 fib,它可以接受一个参数 n,并且可以返回菲波那契数列的
第 n 项。

习题 3.8 定义一个递归函数 mult,它有两个参数 a 和 b,它可以返回 ab,但是只使用 加法。提示: ab 的数学含义是指:b 个 a 相加。

习题 3.9 定义一个递归函数 my_map,使它可以实现 map 的功能。

第 3.8 节 交互式编程

如果你曾经阅读过讲述命令式编程语言的课本的话,你会非常惊奇为什么这本教程到现
在都没有看到一个交互式运行的程序,这在其它语言中可是经常见到的(比如一个询问你的
名字叫什么,然后在屏幕上打印出一句问候语等类似的程序)。
原因其实很简单:一个纯粹的函数型编程语言是不能与外界交互的。因为它不支持“破
坏式更新”,也就无法接受一个用户的输入。破坏式更新将导致副作用的产生,而 Haskell
是尽量避免副作用的。
考虑这种情形:你想编一个可以从用户的键盘上读取一个字符串的函数。那么,如果你
调用两次这个函数,

《未完待续…… flw 于 2005.03.28 日早》

posted on 2006-02-21 11:21  秋雨飘飞  阅读(3969)  评论(3编辑  收藏  举报