HasKell基础入门
Haskell是一种纯函数式编程语言(purely functional programming language)。
在命令式 语言中执行操作需要给电脑安排一组待执行的命令,随着命令的执行,状态会随之发生改变。例如,你将变量a 赋值为5,随后做了其他一些事情之后a 就可能变成其他值。此外,通过控制流结构(如for 循环和while 循环)可以让指令重复执行多次。然而在纯函数式编程语言中一切都与之不同。你不再像命令式语言那样命令电脑“要做什么”,而是通过用函数来描述问题“是什么”,如“阶乘是指从1到某数间所有数字的乘积”,或者“一列数值的总和等同于第一个数值与余下数值的总和相加的结果”。这两个操作都可以表示为函数。 在函数式编程语言中,变量一旦赋值,就不能更改了,你已经声明了a 是5 ,就不能改变主意,再另说a 是别的什么数。做人不能食言,对不? 在纯函数式编程语言中,函数没有任何副作用。函数式编程语言中的函数能做的唯一一件事情就是求值并返回结果。一开始可能觉得这样会很受限,然而好处也正源于此:若以同样的参数调用同一函数两次,得到的结果总是相同的。这被称作引用透明性 (referential transparency)。如此一来,允许你很容易地推论(甚至证明)一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。
Haskell是惰性 (lazy)的。也就是说,若非特殊指明,在真正需要结果以前,Haskell是不会执行函数的。这正是引用透明性的功劳:既然函数的返回值仅与给定的参数相关,那么函数在何时真正执行计算,就无关紧要了。Haskell作为一种惰性语言,基于这条性质,总是尽可能地推迟计算结果。只有当你需要展示计算结果时,Haskell才会执行最少量的计算,到足够展示结果为止。此外,惰性允许我们创建无限长度的数据结构,因为只有需要展示的部分数据结构才会真正被计算。 来看一个展示Haskell惰性的例子。假设你有一个数值的列表xs = [1,2,3,4,5,6,7,8] ,还有一个函数叫做doubleMe ,它可以将列表中的所有元素都乘以2 ,并返回一个新的列表。如果想让整个列表乘以8,你可能会写下这样的代码:
doubleMe(doubleMe(doubleMe(xs)))
在命令式语言中,这会遍历一遍列表,复制一份列表,然后返回。随后还要重复遍历两次、复制两次,才能得到最终的结果。 在惰性语言中,不强迫输出结果的前提下,针对列表调用doubleMe ,程序只会跟你说:“好,我待会做!”一旦你需要查看结果,第一个doubleMe 就会要求第二个doubleMe 马上将结果交给它。然后第二个doubleMe 会向第三个doubleMe 传达同样的话,这时第三个doubleMe 只能不情愿地将1 乘以2 得2 ,交给第二个doubleMe 。第二个doubleMe 再乘以2 得4 ,返回给第一个doubleMe 。第一个doubleMe 再将结果乘以2 ,最终告诉你结果列表中的首个元素是8 。也就是说,因为惰性,这一切只需要遍历一次列表即可,而且仅在你真正需要结果时才会执行。
Haskell是静态类型 (statically typed)的。当你编译程序时,编译器需要明确哪个是数字,哪个是字符串,等等。静态类型意味着很大一部分错误都可以在编译时被发现。比如,若试图将一个数字和字符串相加,编译器就会报错。 Haskell拥有一套强大的类型系统,支持类型推导 (type inference)。这样一来你就不需要在每段代码上都标明它的类型,例如,如果计算a=5+4 ,你就不需要告诉编译器“a 是一个数”,它可以自己推导出来。类型推导让你编写更加通用的程序更容易。假设有个二元函数是将两个数相加,你无需声明类型,这个函数即可对一切可以相加的值进行计算。 Haskell优雅且简洁 。它采纳了许多高级概念,与同等的命令式语言相比,Haskell的代码往往更短。更短就意味着更容易维护,bug也就更少。 Haskell由一组天才的精英分子(很多博士)开发。Haskell的研发工作始于1987年,当时一个学会的研究员齐聚一堂,商讨设计一种强大的编程语言。时至1999年,Haskell Report发布,标志着稳定版本的最终确定。
准备工具
简单讲,开始Haskell的学习,只需要一个编辑器和一个编译器。你可能已经安装了最喜欢的编辑器,在此不加赘述。如今最常用的Haskell编译器是Glasgow Haskell Compiler(GHC),本书也用它。 获取所需工具最简便的方式是下载Haskell Platform。除GHC编译器之外,Haskell Platform更包含了一系列有用的Haskell库!可以参照http://hackage.haskell.org/platform 中的步骤,向你的操作系统中安装Haskell Platform。 GHC除了能够编译Haskell脚本(一般后缀为.hs),也提供了一个交互模式。在这里,你可以装载脚本中的函数,随后直接调用它们即可获得计算的结果。修改代码之后,使用交互模式观察代码的执行结果,要比编译然后执行方便得多。尤其是在学习过程中,交互模式十分有用。
安装Haskell Platform之后,打开一个终端窗口(假定你在使用Linux或者Mac OS X系统)如果你使用的是Windows系统,则打开命令提示符。然后,输入ghci 并按回车键,进入交互模式。如果你的系统没有找到GHCi程序,可以尝试重启计算机。 假设你在一段脚本(如myfunctions.hs)中定义了几个函数,通过:l myfunctions 命令即可将这些函数装载进入GHCi。(需要保证myfunctions.hs位于启动GHCi的同一目录之下。) 一旦修改了这个.hs脚本的内容,再次执行:l myfunctions.hs 或者与之等价的:r ,都可以重新装载该脚本。我本人通常的工作流程就是在.hs 文件中定义几个函数,然后装载到GHCi,对付它,修改文件,再装载,如此重复。这也正是我们将采用的基本流程。
Haskell Platform 主要由三个部分组成,即 GHC(Glasgow Haskell Compiler), cabal 和 stack。
主要推荐的安装方式 使用ghcup进行安装,访问GHCUP主页,复制粘贴安装命令(只有一条)即可。我们测试的结果是可以正常安装,如果不能安装(因为网络问题),请继续使用下面的方法 注意:windows用户建议使用wsl2,不在windows平台上进行安装。 For Linux, macOS, FreeBSD or Windows Subsystem 2 for Linux, run this in a terminal:
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
For Windows, run this in a PowerShell session:
Set-ExecutionPolicy Bypass -Scope Process -Force;[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; try { Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) -ArgumentList $true } catch { Write-Error $_ }
准备学习
首先,我们要做的就是进入GHC的交互模式,调用几个函数,以便我们简单地体验一把Haskel。打开终端,输入ghci,可以看到如下欢迎信息:
GHCi, version 6.12.3: http://www.haskell.org/ghc/
:? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
恭喜,你已经进入了GHCi!试几个简单的运算:
ghci> 2 + 15
17
ghci> 49 * 100
4900
ghci> 1892 - 1472
420
ghci> 5 / 2
2.5
如果在同一个表达式中使用了多个运算符,Haskell会按照运算符优先级顺序执行计算。比如* 的优先级比-高,50*100-4999 就相当于(50 * 100) - 4999 。 也可以通过括号来显式地指定运算次序,就像这样:
ghci> (50 * 100) - 4999
1
ghci> 50 * 100 - 4999
1
ghci> 50 * (100 - 4999)
-244950
很酷,是吧?(哼,我知道一点也不酷,但请容许我自High一下嘛。)
有个小陷阱需要注意:只要表达式中存在负数常量,就最好用括号将它括起来。比如,执行5 * -3 会使GHCi报错,而5*(-3) 就不会有问题。 Haskell中的逻辑运算也同样直白。同许多编程语言一样,Haskell的布尔值为True与False,也有&;&; 用来求逻辑与(布尔与运算)、||用来求逻辑或(布尔或运算),以及not 用来对True 或者False 取反。
ghci> True && False
False
ghci> True && True
True
ghci> False || True
True
ghci> not False
True
ghci> not (True && True)
False
也可以通过==与/=来判断两个值相等还是不等:
ghci> 5 == 5
True
ghci> 1 == 0
False
ghci> 5 /= 5
False
ghci> 5 /= 4
True
ghci> "hello" == "hello"
True
在同一个表达式中混用不一样的值要十分小心!如果我们输入了5+”llama” 这样的代码,就会得到下面这样的出错消息了:
No instance for (Num [Char])
arising from a use of `+' at <interactive> :1:0-9
Possible fix: add an instance declaration for (Num [Char])
In the expression: 5 + "llama"
In the definition of `it': it = 5 + "llama"
GHCi 告诉我们,”llama” 并不是数值,所以它不知道怎样才能给它加上5。+运算符要求它的两个参数都是数值才行。
另一方面,==可以比较任何两个可以比较的值,唯一标准就是:这两个值的类型必须相同。比如,我们如果输入True==5 ,GHCi就会报错。 注意: 5+4.0 是合法的表达式,虽说4.0 不是整数,但5 既可以被看做整数也可以被看做浮点数。这样看,5 与浮点数4.0 的类型就匹配起来了。
调用函数
也许你并未察觉,从始至终我们一直都在使用函数。*就是一个函数,它可以将两个数相乘。可见,在应用(又称调用)这个函数时,我们就像夹三明治一样用两个参数将它夹在中央,这种函数被称作中缀函数 (infix function)。
至于其他的大部分函数,则属于前缀函数 (prefix function)。在Haskell中调用前缀函数时,先是函数名和一个空格,后跟参数列表(也是由空格分隔)。简单举个例子,我们尝试调用Haskell中最无聊的函数succ :
ghci> succ 8
9
succ 函数可以取得参数的后继,前提是它要有明确的后继。一个整数的后继,就是比它大的下一个数了。
接下来尝试调用两个前缀函数min 和max :
ghci> min 9 10
9
ghci> min 3.4 3.2
3.2
ghci> max 100 101
101
min 和max 接受两个可比较大小的参数(如数),相应地返回较大或者较小的那个数。
在Haskell中,函数应用拥有最高的优先级。因而如下两句是等效的:
ghci> succ 9 + max 5 4 + 1
16
ghci> (succ 9) + (max 5 4) + 1
16
也就是说,取9乘10的后继,简单地像下面这样写是不行的:
ghci> succ 9 * 10
因为运算有优先级,这种写法相当于先取9的后继(得到10),然后再乘以10得100。要得到正确的写法,应该改成这样:
ghci> succ (9 * 10)
得91。 如果某函数有两个参数,也可以用反引号(`)将它括起,以中缀函数的形式调用它。比如,div函数可以用来求两个整数的商,如下:
ghci> div 92 10
9
但这种形式并不容易理解:究竟是哪个数是除数,哪个数被除数?用上反引号,按照中缀函数的形式调用它就清晰多了:
ghci> 92 div 10
9
从命令式编程走过来的程序员往往觉得函数调用离不开括号,以至于一下子无法接受Haskell的风格。其实很简单:只要见到bar (bar 3) 之类的东西,就是说以3 为参数调用bar 函数,随后用所得的结果再次调用bar 。对应的C代码大约就是这样:bar(bar(3)) 。
第一个函数
函数的声明与它的调用形式大体相同,都是先函数名,后跟由空格分隔的参数表。不过后面多了个等号(=),并且后面的代码定义了函数的行为。
举个例子,我们先写一个简单的函数,让它将一个数字乘以2。打开你最喜欢的编辑器,输入如下代码:
doubleMe x = x + x
将它保存为baby.hs或者任意名称,然后转至文件所在目录,打开GHCi,执行:l baby 装载它。随后就可以跟我们的函数小朋友玩耍了:
ghci> :l baby
[1 of 1] Compiling Main ( baby.hs, interpreted )
Ok, modules loaded: Main.
ghci> doubleMe 9
18
ghci> doubleMe 8.3
16.6
+运算符对整数和浮点数都可用(实际上所有有数字特征的值都可以),所以我们的函数可以处理一切数值。
接下来声明一个取两个参数的函数,让它分别将两个参数乘以2再相加。修改baby.hs,将如下代码加到后面:
doubleUs x y = x * 2 + y * 2
注意: Haskell中的函数定义并没有顺序的概念,所以baby.hs中函数定义的先后对程序没有任何影响。 将它保存,在GHCi中输入:l baby再次装载。测试它的结果是否符合预期:
ghci> doubleUs 4 9
26
ghci> doubleUs 2.3 34.2
73.0
ghci> doubleUs 28 88 + doubleMe 123
478
你也可以在函数中调用其他的函数,如此一来我们可以将doubleUs 函数改为:
doubleUs x y = doubleMe x + doubleMe y
这种模式在Haskell中十分常见:编写一些明显正确的简单函数,然后将它们组合起来,形成一个较为复杂的函数。这是减少重复工作的金科玉律。设想,如果哪天有个数学家验证说2其实该是3,我们该怎么改?在这里,我们只需要将doubleMe 改为x+x+x 即可,由于doubleUs 调用doubleMe ,于是整个程序便轻松进入2即是3的古怪世界。 下面我们再编写一个函数, 它将小于等于100的数都乘以2 (因为大于100的数都已经足够大了)。
doubleSmallNumber x = if x > 100
then x
else x*2
这个例子就引出了Haskell的if 语句。你也许已经对其他语言的else 很熟悉,不过Haskell的if 语句的与众不同之处就在于,else 部分是不可省略的。 在命令式语言中,程序的执行就是一步又一步的操作,if 语句可以没有else 部分,如果条件不符合,就直接跳过这一步。因此,命令式语言中的if 语句可以什么都不做。 而在Haskell中,程序是一系列函数的集合:函数取数据作为参数,并将它们转为想要的结果。每个函数都会返回一个结果,也都可以为其他函数所用。既然必须返回结果,那么每个if 就必须同时跟着一个else ,不管条件满足还是失败,都需要返回一个结果。一言以蔽之,Haskell中的if 是一个必然返回结果的表达式 (expression),而非语句 (statement)。
假如我们想让之前的doubleSmallNumber 函数的结果都加1,新的函数的定义将是如下的模样:
doubleSmallNumber' x = (if x > 100 then x else x*2) + 1
可以留意这里括号的使用,如果忽略掉括号,函数就会只在x 小于等于100时给结果加1了。另外,也可以留意函数名最后的那个单引号,它没有任何特殊含义,只是一个函数名的合法字符罢了。通常我们会使用单引号来区分这是某函数的严格求值 (与惰性求值相对)版本,或者是一个稍经修改但差别不大的函数。 既然’是合法字符,定义这样的函数也是可以的:
conanO'Brien = "It's a-me, Conan O'Brien!"
在这里有两点需要注意。首先就是我们没有大写Conan的首字母,因为函数是不能以大写字母开始的(我们将在后面讨论其原因 0,另外就是这个函数并没有任何参数。没有参数的函数通常被称作定义 或者名字 ,在函数被定义之后我们就再也不能修改它的内容,conanO’Brien从此与字符串”It’s a-me, Conan O’Brien!” 完全等价。
列表入门
在Haskell中,列表是一种单类型的 (homogeneous)数据结构,可以用来存储多个类型相同的元素。我们可以在里面装一组数字或者一组字符,但不能把字符和数字装在一起。 列表由方括号括起,其中的元素用逗号分隔开来:
ghci> let lostNumbers = [4,8,15,16,23,42]
ghci> lostNumbers
[4,8,15,16,23,42]
注意:在GHCi中,可以使用let 关键字来定义一个常量。在GHCi中执行let a = 1 与在脚本中编写a=1 随后用:l 装载的效果是等价的。
拼接列表
拼接两个列表可以算是最常见的操作之一了,在Haskell中这可以通过++运算符实现:
ghci> [1,2,3,4] ++ [9,10,11,12]
[1,2,3,4,9,10,11,12]
ghci> "hello" ++ " " ++ "world"
"hello world"
ghci> ['w','o'] ++ ['o','t']
"woot"
注意:Haskell中的字符串实际上就是一组字符组成的列表。”hello” 只是[‘h’,’e’,’l’,’l’,’o’] 的语法糖而已。因此,我们可以使用处理列表的函数来对字符串进行操作。
在使用++运算符处理长字符串时要格外小心(对长的列表也是同样),Haskell会遍历整个第一个列表(++符号左边的列表)。在处理较短的字符串时问题还不大,但要是在一个长度为5000万的列表上追加元素,那可得执行好一会儿了。
不过,仅仅将一个元素插入列表头部的成本几乎为零,这里使用: 运算符(被称作Cons [1] 运算符)即可:
ghci> 'A':" SMALL CAT"
"A SMALL CAT"
ghci> 5:[1,2,3,4,5]
[5,1,2,3,4,5]
可以留意,第一个例子中,取一个字符和一个字符构成的列表(即字符串)作为参数;第二个例子很相似,取一个数字和一个数字组成的列表作为参数。:运算符的第一个参数的类型一定要与第二个参数中列表中元素的类型保持一致。
而++运算符总是取两个列表作为参数。若要使用++运算符拼接单个元素到一个列表的最后,必须用方括号把它括起来,使之成为单元素的列表。
ghci> [1,2,3,4] ++ [5]
[1,2,3,4,5]
这里如果写成[1,2,3,4] ++ 5 就错了,因为++ 运算符的两边都必须是列表才行,这里的5是个数,不是列表。
有趣的是,[1,2,3] 实际上是1:2:3:[] 的语法糖。[]表示一个空列表,若从前端插入3 ,它就成了[3] ,再插入2 ,它就成了[2,3] ,依此类推。 注意:[] 、[[]] 和[[] , [] , []] 是不同的。第一个是一个空的列表,第二个是含有一个空列表的列表,第三个是含有三个空列表的列表。
访问列表中的元素
若是要按照索引取得列表中的元素,可以使用!!运算符,下标从0 开始。
ghci> "Steve Buscemi" !! 6
'B'
ghci> [9.4,33.2,96.2,11.2,23.25] !! 1
33.2
但你如果想在一个只含有4个元素的列表中取它的第6个元素,就会得到一个错误。要小心!
嵌套列表
列表同样也可以以列表为元素,甚至是列表的列表的列表: ghci> let b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]] ghci> b [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]] ghci> b ++ [[1,1,1,1]] [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]] ghci> [6,6,6]:b [[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]] ghci> b !! 2 [1,2,2,3,4] 列表中的列表可以是不同长度的,但类型必须相同。与不能在同一个列表中混合放置字符和数组一样,混合放置数的列表和字符的列表也是不可以的。
比较列表
只要列表内的元素是可以比较的,那就可以使用<、>、>= 和 <= 来比较两个列表的大小。 它会按照字典顺序,先比较两个列表的第一个元素,若它们的值相等,则比较二者的第二个元素,如果第二个仍然相等,再比较第三个,依此类推,直至遇到不同为止。两个列表的大小以二者第一个不等的元素的大小为准。
比如,执行[3, 4, 2] < [3, 4, 3] 时,Haskell就会发现3 与3 相等,随后比较4 与4 ,依然相等,再比较2 与3 ,这时就得出结论了:第一个列表比第二个列表小。>、>=与<=也是同样的道理。
ghci> [3,2,1] > [2,1,0]
True
ghci> [3,2,1] > [2,10,100]
True
ghci> [3,4,2] < [3,4,3]
True
ghci> [3,4,2] > [2,4]
True
ghci> [3,4,2] == [3,4,2]
True
此外,非空列表总认为比空列表更大。这样即可保证两个列表总是可以顺利地做比较,即使其中一个列表完全等同于另一个列表的开头部分。
更多列表操作
下面是几个有关列表的常用函数,稍带用途与示例。
head 函数返回一个列表的头部,也就是列表的第一个元素。
ghci> head [5,4,3,2,1]
5
tail 函数返回一个列表的尾部,也就是列表除去头部之后的部分:
ghci> tail [5,4,3,2,1]
[4,3,2,1]
last 函数返回一个列表的最后一个元素。
ghci> last [5,4,3,2,1]
1
init 函数返回一个列表除去最后一个元素的部分。
ghci> init [5,4,3,2,1]
[5,4,3,2]
length 函数返回一个列表的长度:
ghci> length [5,4,3,2,1]
5
null 函数检查一个列表是否为空。如果是,则返回True;否则返回False:
ghci> null [1,2,3]
False
ghci> null []
True
reverse 函数将一个列表反转:
ghci> reverse [5,4,3,2,1]
[1,2,3,4,5]
take 函数取一个数字和一个列表作为参数,返回列表中指定的前几个元素,如下:
ghci> take 3 [5,4,3,2,1]
[5,4,3]
ghci> take 1 [3,9,3]
[3]
ghci> take 5 [1,2]
[1,2]
ghci> take 0 [6,6,6]
[]
如上,若是试图取超过列表长度的元素个数,只能得到原先的列表。若取0个元素,就会得到一个空列表。
drop 函数的用法大体相同,不过它是删除一个列表中指定的前几个元素:
ghci> drop 3 [8,4,2,1,5,6]
[1,5,6]
ghci> drop 0 [1,2,3,4]
[1,2,3,4]
ghci> drop 100 [1,2,3,4]
[]
maximum 函数取一个列表作为参数,并返回最大的元素,其中的元素必须可以做比较。minimum 函数与之相似,不过是返回最小的元素。
ghci> maximum [1,9,2,3,4]
9
ghci> minimum [8,4,2,1,5,6]
1
sum 函数返回一个列表中所有元素的和。product 函数返回一个列表中所有元素的积。
ghci> sum [5,2,1,6,3,2,5,7]
31
ghci> product [6,2,1,2]
24
ghci> product [1,2,5,6,7,9,2,0]
0
elem 函数可用来判断一个元素是否包含于一个列表,通常以中缀函数的形式调用它,这样更加清晰。
ghci> 4 elem [3,4,5,6]
True
ghci> 10 elem [3,4,5,6]
False
区间
该怎样得到一个由1~20所有数组成的列表呢?我们完全可以用手把它们全都录入一遍,但显而易见,这并不是完美人士的方案,完美人士都用区间 (range)。区间是构造列表的方法之一,而其中的值必须是可枚举的 ,或者说,是可以排序的。 例如,数字可以枚举为1、2、3、4等。字符同样也可以枚举:字母表就是A~Z所有字符的枚举。然而人名就不可以枚举了,“John”后面是谁?我不知道。 要得到包含1~20 中所有自然数的列表,只要录入[1..20] 即可,这与录入[1,2,3,4,5,6,7,8, 9,10,11,12,13,14,15,16,17,18,19,20] 完全等价。两者的唯一区别是手写一串非常长的列表比较笨。
下面是一些例子:
ghci> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
ghci> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> ['K'..'Z']
"KLMNOPQRSTUVWXYZ"
区间很聪明,允许你告诉它一个步长。要得到1~20中所有的偶数,或者3的倍数该怎样?只要用逗号将前两个元素隔开,再标上区间的上限就好了:
ghci> [2,4..20]
[2,4,6,8,10,12,14,16,18,20]
ghci> [3,6..20]
[3,6,9,12,15,18]
尽管区间很聪明,但它恐怕还是难以满足人们对它过分的期许。比如,你就不能通过[1,2,4,8,16..100] 这样的语句来获得100以下的所有2的幂。一方面是因为步长只能标明一次,另一方面就是仅凭前几项,数组后面的项也有可能是无法确定的。
注意: 要得到从20到1之间的列表,[20..1] 是不可以的,必须得[20,19..1] 。对于没有提供步长的区间(如[20..1] ),Haskell会先构造一个空的列表,随后从区间的下限开始,不停地赠长,直到大于等于上限为止。既然20已经大于1了,那么所得的结果只能是个空列表。
你也可以不标明区间的上限,从而得到一个无限长度的列表。在后面我们会讲解关于无限列表的更多细节。取前24个13的倍数该怎样?下面是一种方法:
ghci> [13,26..24*13]
[13,26,39,52,65,78,91,104,117,130,143,156,169,182,195,208,221,234,247,260,273,286,
299,312]
但有更好的方法——使用无限长度的列表:
ghci> take 24 [13,26..]
[13,26,39,52,65,78,91,104,117,130,143,156,169,182,195,208,221,234,247,260,273,286,
299,312]
由于 Haskell 是惰性的 ,它不会对无限长度的列表直接求值(不然会没完没了)。它会等着,看你会从它那儿取哪些元素。在这里它见你只要前24个元素,便欣然交差。
下面是几个生成无限列表的函数。
cycle 函数接受一个列表作为参数并返回一个无限列表。
ghci> take 10 (cycle [1,2,3])
[1,2,3,1,2,3,1,2,3,1]
ghci> take 12 (cycle "LOL ")
"LOL LOL LOL "
repeat 函数接受一个值作为参数,并返回一个仅包含该值的无限列表。这与用cycle 处理单元素列表的效果差不多。
ghci> take 10 (repeat 5)
[5,5,5,5,5,5,5,5,5,5]
若只是想得到包含相同元素的列表,直接使用replicate 函数将更加简单,它取一个参数表示列表的长度,一个参数表示列表中要复制的元素:
ghci> replicate 3 10
[10,10,10]
最后,在区间中使用浮点数要格外小心!浮点数依据定义,只能实现有限的精度。若是在区间中使用浮点数,你就会得到如下的糟糕结果:
ghci> [0.1, 0.3 .. 1]
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]
列表推导式
列表推导式 (list comprehension)是一种过滤、转换或者组合列表的方法。 学过数学的你对集合推导式 (set comprehension)概念一定不会陌生。通过它,可以从既有的集合中按照规则产生一个新集合。前10个偶数的集合推导式可以写为{2 · x | x ∈N , x ≤ 10},先不管语法,它的含义十分直观:“取所有小于等于10的自然数,各自乘以2,将所得的结果组成一个新的集合。” 若要在Haskell中实现上述表达式,我们可以通过类似take 10 [2,4..] 的代码来实现。不过,使用列表推导式也是同样的轻而易举:
ghci> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]
我们可以再仔细看看这段代码,理解一下列表推导式的语法。 在[x*2 | x <- [1..10]] 这段代码中,我们通过[x <- [1..10]] 取了[1..10] 这个列表中的每一项元素,x 即[1..10] 中的每一项元素的值,也可以说,x 是[1..10] 中每一项元素的绑定 。竖线(|)前面的部分指列表推导式的输出,表示所取的值与计算结果的映射关系。在这个例子里,我们就是取[1..10] 中的所有数字的2倍了。 看起来,这要比第一个例子长得多,也复杂得多。但是让所有数字乘以2只是一种简单的情景,如果遇到更复杂的情况该怎么办?这才是列表推导式大显身手的地方。
比如,我们想给这个列表推导式再添一条谓词 (predicate)。它位于列表推导式最后面,与前面的部分由一个逗号分隔。在这里,我们只取乘以2后大于等于12的元素。
ghci> [x*2 | x <- [1..10], x*2 >= 12]
[12,14,16,18,20]
若是取50~100中所有除7的余数为3的元素该怎么办?很简单:
ghci> [ x | x <- [50..100], x `mod` 7 == 3]
[52,59,66,73,80,87,94]
注意: 从一个列表中筛选出符合特定谓词的元素的操作,也称为过滤(filter)。 再举个例子,假如我们想要一个列表推导式,它能够使列表中所有大于10的奇数变为”BANG” ,小于10的奇数变为”BOOM” ,其他则统统扔掉。方便起见,我们将这个推导式置于一个函数之中,使它易于重用。
boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]
注意: 记住,如果是在GHCi中定义这个函数,必须在函数名前面放一个let 关键字。不过将函数写在脚本里再装载到GHCi的话就不必再加let 了。
odd 函数判断一个数是否为奇数:如果是,返回True ;否则返回False 。某项元素只有满足所有谓词时,才会被列表推导式留下。
ghci> boomBangs [7..13]
["BOOM!","BOOM!","BANG!","BANG!"]
也可以加多个谓词,中间用逗号隔开。比如,取10~20中所有不等于13、15或19的数,可以这样:
ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19]
[10,11,12,14,16,17,18,20]
除了多项谓词之外,从多个列表中取元素也是可以的。当分别从多个列表中取元素时,将得到这些列表中元素的所有组合:
ghci> [x+y | x <- [1,2,3], y <- [10,100,1000]]
[11,101,1001,12,102,1002,13,103,1003]
这里的x 取自[1, 2, 3] ,y 取自[10, 100, 1000] 。随后这两个列表按照如下的方式组合:首先x 成为1,同时y 分别取[10, 100, 1000] 中的每一个值。由于列表推导式的输出为x+y ,可得到11、101、1001作为结果的开头部分(即1分别与10、100与1000相加的结果)。随后x成为2,同理可得12、102与1002,追加到结果的后面。对3也同理。 按照这一规律,[1,2,3] 中的每个元素都与[10,100,1000] 中的元素按照所有可能的方式相组合,再按x+y 取得所有这些组合的和。 下面是另一个例子。假设有两个列表,即[2,5,10] 和[8,10,11] ,要取它们所有组合的积,可以写出这样的列表推导式:
ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
[16,20,22,40,50,55,80,100,110]
意料之中,得到的新列表长度为9。若只取乘积大于50的结果该怎样?只需要再加一条谓词:
ghci> [ x*y | x <-[2,5,10], y <- [8,10,11], x*y > 50]
[55,80,100,110]
我们再取一个包含一组名词和形容词的列表推导式吧,写诗的话也许用得着。
ghci> let nouns = ["hobo","frog","pope"]
ghci> let adjectives = ["lazy","grouchy","scheming"]
ghci> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]
["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog",
"grouchy pope","scheming hobo", "scheming frog","scheming pope"]
我们甚至可以通过列表推导式来编写自己的length 函数!就叫它length’ 。这个函数首先将列表中的每个元素替换为1,随后通过sum 将它们加起来,从而得到列表长度。
length' xs = sum [1 | _ <- xs]
这里我们用了一个下划线(_)作为绑定列表中元素的临时变量,表示我们并不关心它具体的值。 记住,字符串也是列表,完全可以使用列表推导式来处理字符串。下面是一个除去字符串中所有非大写字母的函数:
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]
在这里,主要的工作由谓词完成。它给出了保证:只有位于[‘A’..’Z’] 这一区间之内的字符才被视为是大写。我们可以将这个函数装载到GHCi测试一下:
ghci> removeNonUppercase "Hahaha! Ahahaha!"
"HA"
ghci> removeNonUppercase "IdontLIKEFROGS"
"ILIKEFROGS"
对于列表的列表可以通过嵌套的列表推导式处理。假设有一个包含许多数值的列表的列表,让我们在不拆开它的前提下除去其中的所有奇数:
ghci> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]]
ghci> [ [ x | x <- xs, even x ] | xs <- xxs]
[[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]
这里的输出部分嵌套了另一个列表推导式。已知列表推导式的返回结果永远是列表,因而可以看出,这里的返回结果是一个由数值组成的列表的列表。 注意: 为提升可读性,将列表推导式分成多行也是可以的。若非在GHCi之下,还是将列表推导式分成多行比较好,尤其是需要嵌套的时候。
元组
元组 (tuple)允许我们将多个异构的值组合成为一个单一的值。 从某种意义上讲,元组很像列表。但它们却有着本质的不同。首先就像前面所说,元组是异构的,这表示单个元组可以含有多种类型的元素。其次,元组的长度固定,在将元素存入元组的同时,必须明确元素的数目。 元组由括号括起,其中的项由逗号隔开。
ghci> (1, 3)
(1,3)
ghci> (3, 'a', "hello")
(3,'a',"hello")
ghci> (50, 50.4, "hello", 'b')
(50,50.4,"hello",'b')
### 使用元组 为理解元组的用途,我们可以思考一下Haskell中二维向量的表示方法。使用列表是可以的,按照[x, y] 的形式,它倒也工作良好。若要将一组向量置于一个列表中来表示二维坐标系中的平面图形又该怎样?我们可以写个列表的列表,像这样:[[1,2],[8,11],[4,5]] 。 但是,如果遇到[[1,2],[8,11,5],[4,5]] 这样的列表并把它当做向量列表来使用,这种方法就有问题了。它并不是合法的向量列表,却是一个合法的列表的列表,毕竟其中元素的类型都相同(数值的列表组成的列表)。有这种情况存在,编写处理向量与图形的函数将复杂得多。 然而长度为2的元组(也称作序对 ,pair)与长度为3的元组(也称作三元组,triple)被视为不同的类型。这便意味着一个包含一组序对的列表不能再加入一个三元组。基于这个性质,使用元组来表示向量无疑更加合适。 要将原先的向量改为用元组表示,可以把里面的方括号改成括号,像这样[(1, 2), (8, 11), (4, 5)] 。如果不小心将二元组与三元组混到了一起,就会报出以下的错误:
ghci> [(1,2),(8,11,5),(4,5)]
Couldn't match expected type `(t, t1)'
against inferred type `(t2, t3, t4)'
In the expression: (8, 11, 5)
In the expression: [(1, 2), (8, 11, 5), (4, 5)]
In the definition of `it': it = [(1, 2), (8, 11, 5), (4, 5)]
同样,即使两个元组的长度相同,但其中的元素的类型不一样,Haskell也会将它们视为不同的类型。比如[(1,2),(“one”,2)] 这样的列表就有问题,因为其中的第一个元组是一对数,而第二个元组却成了一个字符串和一个数。 元组可以方便地用来表示一组数据的关联关系。比如,我们要表示一个人的姓名与年龄,可以使用这样的三元组:(“Christopher”, “Walken”, 55) 。 需要记住,元组是固定大小的——使用元组时应事先了解它里面含有多少项。乍一接触可能会觉得挺死板,不过每个不同长度的元组都是独立的类型,这也就意味着你无法写一个通用的函数为它追加元素。而只能单独写一个将元素与二元组构成三元组的函数、将元素与三元组构成四元组的函数,以及将元素与四元组构成五元组的函数,依此类推。 同列表相同,只要其中的项是可比较的,元组也可以比较大小,只是不可以像比较不同长度的列表那样比较不同长度的元组。 可以有单元素的列表,但元组不行。可以这样想:单元素的元组的性质与它里面包含的元素本身几乎一模一样,那么徒增一个新的类型又有什么用处呢?
使用序对
在Haskell中,将数据保存在序对里十分常见。对此,Haskell内置了许多有用的函数来处理序对。下面给出的是其中的两个函数。
fst 取一个序对作为参数,返回其首项。
ghci> fst (8,11)
8
ghci> fst ("Wow", False)
"Wow"
snd 取一个序对作为参数,返回其尾项。
ghci> snd (8,11)
11
ghci> snd ("Wow", False)
False
注意: 这两个函数仅对序对有效,而不能应用于三元组、四元组和五元组之上。稍后,我们将过一遍从元组中取数据的所有方式。
zip 函数可以用来酷酷地生成一组序对的列表。它取两个列表作为参数,然后将它们交叉配对,形成一组序对。它的外表很简单,不过当你需要同时遍历两个列表时,就可以体会到它的实用。比如:
ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]
注意,由于序对中可以含有不同的类型,zip 函数也可以将不同类型的序对组合在一起。不过,若要组合两个不同长度的列表会怎么样?
ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
[(5,"im"),(3,"a"),(2,"turtle")]
可见,较长的列表会在中间断开,至较短的列表结束为止,余下的部分就直接忽略掉了。而且,由于Haskell是惰性的,我们也可以使用zip组合有限的和无限的列表:
ghci> zip [1..] ["apple", "orange", "cherry", "mango"]
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]
找直角三角形
接下来思考一道同时用到元组与列表推导式的题目,使用Haskell来找到所有满足下列条件的直角三角形:
三边长度皆为整数; 三边长度皆小于等于10; 周长(三边之和)为24的直角三角形。
如果三角形中有一个角是直角,那它就是一个直角三角形。直角三角形有一条重要的性质,那就是直角的两条边的平方和等于对边的平方。如图所示,两条直角边分别标记为a 和b ,直角的对边标记为c 。其中c 也被称作斜边。
首先,把所有三个元素都小于等于10的元组都列出来:
ghci> let triples = [ (a,b,c) | c <- [1..10], a <- [1..10], b <- [1..10] ]
我们在三个列表中取值,并且左侧的输出部分将它们组合为一个三元组。随后在GHCi下边执行triples,可得到一个含有1000个元素的列表,就不在这里列出了。
我们接下来给它添加一个过滤条件,使其符合勾股定理(a 2 +b 2 =c 2 )。同时也考虑上a边要短于斜边(c 边),b 边要短于a 边情况:
ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], a <- [1..c], b <- [1..a],
a^2 + b^2 == c^2]
注意,在这里我们限制了求解的区间,不再检查多余的三元组,比如b边比斜边长的情形(直角三角形中,斜边总比直角边长),同时假定b边总不长于a边。这对结果是没有影响的,即使忽略掉所有使a^2+b^2=c^2 且b>a 的(a, b, c) ,还有(b, a, c) 在——它们实际上同一个三角形,只是直角边的顺序相反罢了。否则,我们得到的列表将无端多出一半重复的三角形。 注意: 在GHCi中,不允许将定义与表达式拆为多行。不过在本书中,因为版面的原因,我们偶尔不得不将一行代码折行。(不然这本书将特别特别宽,宽到放不到普通的书架上了——读者也就只有买个大书架才行呢。) 已经差不多了。最后修改函数,告诉它只要周长为24的三角形。
ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], a <- [1..c], b <- [1..a],
a^2 + b^2 == c^2, a+b+c == 24]
ghci> rightTriangles'
[(6,8,10)]
得到正确结果!这便是函数式编程的一般思路:先取一个初始的集合并将其变形,随后持续地利用过滤条件缩小计算范围,最终取得一个(或多个)最终解。