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 中文教程 中文资料
致各位网友:如有评论,请贴在本文最前面,不要贴在翻译正文中,不然有可能会被覆盖掉。
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 节继续作进一步的讨论。
习题
作为一个程序员,我们不可能总是像在上面那样交互式地输入一些表达式来计算,我们
可能更加需要坐下来用自己喜爱的编辑器编写一段代码,然后保存成文件再来重复使用它。
在第 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 ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
现在,我们已经知道如何在文件中书写代码了,下面我们开始学习如何书写函数。正如
你所预料的一样,函数是 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 的时候,实际上是
先计算 54 然后把计算结果再和 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 中都定义了哪些函数。不然,也
许我们一不小心就重写了已经存在的函数。请看下面:
下面我们演练一下:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ 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 显示的错误。
我们尝尝希望能在我们的函数内部进行一个局域范围内的定义。譬如说,如果你还记得
读中学时数学课上学过的一元二次方程的求根公式的话,那么我们可以用下面的函数获得任
意一个一元二次方程 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(bb 4ac) 定义成一个局域绑定,好比就叫做 "det",然后我们就可
以把所有出现 sqrt(bb - 4ac) 的地方都用 "det" 来代替,请看下面:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ roots a b c = ┃
┃ let det = sqrt(b
b - 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 ) ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
中缀函数是指那些由符号而不是字母组成的函数。例如 () (-) () (+) 等都是中缀
函数。你也可以使用它们的“非中缀”形式。请看下面的两个例子:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Prelude> 5 + 10 ┇
┇ 15 ┇
┇ Prelude> (+) 5 10 ┇
┇ 15 ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
反过来,非中缀形式的函数也可以通过附加反引号 `` 来使用它们的中缀形式:
┏┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┓
┇ Prelude> map Char.toUpper "Hello World" ┇
┇ "HELLO WORLD" ┇
┇ Prelude> Char.toUpper `map` "Hello World" ┇
┇ "HELLO WORLD" ┇
┗┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┛
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 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
上面这个例子程序同时演示了行注释、块注释、嵌入式注释。
在命令式语言如 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 章。
┏━━━━━━━━━━━━━━━━┓
┃ 习题 ┃
┗━━━━━━━━━━━━━━━━┛
/ 1 当 n = 1 或者 n = 2 时
F (n) = |
\ F (n-2) + F (n-1) 其它情况
请写一个递归函数 fib,它可以接受一个参数 n,并且可以返回菲波那契数列的
第 n 项。
如果你曾经阅读过讲述命令式编程语言的课本的话,你会非常惊奇为什么这本教程到现
在都没有看到一个交互式运行的程序,这在其它语言中可是经常见到的(比如一个询问你的
名字叫什么,然后在屏幕上打印出一句问候语等类似的程序)。
原因其实很简单:一个纯粹的函数型编程语言是不能与外界交互的。因为它不支持“破
坏式更新”,也就无法接受一个用户的输入。破坏式更新将导致副作用的产生,而 Haskell
是尽量避免副作用的。
考虑这种情形:你想编一个可以从用户的键盘上读取一个字符串的函数。那么,如果你
调用两次这个函数,
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ASP.NET Core 模型验证消息的本地化新姿势
· 开发的设计和重构,为开发效率服务
· 从零开始开发一个 MCP Server!
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想
· Ai满嘴顺口溜,想考研?浪费我几个小时
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)