简介
关于这个教程
欢迎来到haskell趣学指南!阅读此文表明你正要学haskell。很好,来对地方了,先容我简单介绍一下这个教程。
编写这个教程,一方面是为了巩固我自己对haskell的理解,另一方面也是希望能够分享我的经验,给初学者提供一定帮助。网上现有的haskell教程已经汗牛充栋,在我学习的时候就曾参阅过许多教程与文章,它们讲解问题的思路各不相同,综合的阅读使得我最终能够整理起知识的碎片并正确地理解。所以说,编写这个教程也是创造另一个学习资源的尝试,给读者增加一个选择的余地。
本教程主要是面向已经有命令式编程经验(C, C++, Java, Python …) 、却未曾接触过函数式编程 (Haskell, ML, OCaml …)的读者。还没有编程基础?没关系,像你这样的聪明小伙一定能够学会haskell!
若在学习中遇到什么地方不明白,freenode上的#haskell频道是提问的绝佳去处。那儿的人们友善,耐心且照顾新人。
在我掌握haskell之前的学习曾经失败过两次,它看起来太不可思议,难以理解。不过随后突然灵光一闪,马上就开窍了,往后的学习也就变得游刃有余。我想说的就是:haskell很棒,如果你喜欢编程,那就得好好学学--尽管在咋一看它可能会显得很别扭--它迫使你换个脑筋思考,很有趣!
好,下一节。。。
那么,haskell是啥?
haskell是一门纯函数式编程语言。在命令式语言中执行操作需要给电脑安排一组命令,随着命令的执行,状态就会随之发生改变。例如你给变量a赋值为5,而随后做了其它一些事情之后a就可能变成的其它值。有控制流程,你就可以重复执行操作。然而在函数式编程语言中,你不是像命令式语言那样命令电脑“要做什么”,而是通过用函数来描述出问题“是什么”,如“阶乘是指从1到某数间所有数字的乘积”。变量一旦赋值,就不可以更改了,你已经说了a就是5,就不能再另说a是别的什么数。做人不能食言,对不?所以说,函数式编程语言中的函数能做的唯一事情就是求值,因而没有副作用。一开始会觉得这很受限,不过好处也正源于此:若以同样的参数调用同一函数两次,得到的结果总是相同。这被称作“引用透明”。如此一来编译器就可以理解程序的行为,你也很容易就能验证一个函数的正确性,继而可以将一些简单的函数组合成更复杂的函数。
haskell是惰性的。也就是说若非特殊指明,函数在真正需要结果以前不会被求值。再加上引用透明,你就可以把程序仅看作是数据的一系列变形。如此一来就有了很多有趣的特性,如无限长度的数据结构。假设你有一个List:xs = [1,2,3,4,5,6,7,8],还有一个函数doubleMe,它可以将一个List中的所有元素都乘以二,返回一个新的List。若是在命令式语言中,把一个List乘以8,执行doubleMe(doubleMe(doubleMe(xs))),得遍历三遍xs才会得到结果。而在惰性语言中,调用doubleMe时并不会立即求值,它会说“嗯嗯,待会儿再做!”。不过一旦要看结果,第一个doubleMe就会对第二个说“给我结果,快!”第二个doubleMe就会把同样的话传给第三个doubleMe,第三个doubleMe只能将1乘以2得2后交给第二个,第二个再乘以2得4交给第一个,最终得到第一个元素8。也就是说,这一切只需要遍历一次list即可,而且仅在你真正需要结果时才会执行。惰性语言中的计算只是一组初始数据和变换公式。
haskell 是静态类型的。当你编译程序时,编译器需要明确哪个是数字,哪个是字符串。这就意味着很大一部分错误都可以在编译时被发现,若试图将一个数字和字符串相加,编译器就会报错。haskell拥有一套强大的类型系统,支持自动类型推导。这一来你就不需要在每段代码上都标明它的类型,像计算a=5+4,你就不需另告诉编译器“a是一个数值”,它可以自己推导出来。类型推导可以让你的程序更加简练。假设有个二元函数是将两个数值相加,你就无需声明其类型,这个函数可以对一切可以相加的值进行计算。
haskell采纳了很多高级概念,因而它的代码优雅且简练。与同层次的命令式语言相比,haskell的代码往往会更短,更短就意味着更容易理解,bug也就更少。
haskell的研发工作始于1987年,当时是一个学会的精英分子(很多PhD哦)聚到一块儿,商量着要设计一门牛X的语言。03年,《 Haskell Report 》发布,标志着稳定版本的最终确定。
你需要...
一个编辑器和一个编译器。你可能已经安装了最喜欢的编辑器,在此不加赘述。如今最常用的haskell编译器是GHC和hugs,在本教程中我们将使用ghc。安装的细节就不消多说了,在windows下只要下载一个installer然后一路next最后重启一下(貌似不需要重启,译者注)即可;在基于debain的linux系统下一个apt-get install ghc6 libghc6-mtl-dev看着玩就是了;我没mac电脑,不过听说你如果安装了macPort,就可以通过sudo port install ghc来获得ghc。嗯,应该可以用那古怪的单键鼠标搞haskell吧,我拿不准。
入门
各就各位,预备!
好的,出发!如果你就是那种从不看说明书的不良人士,我推荐你还是回头看一下简介的最后一节。那里面讲了这个教程中你需要用到的工具及基本用法。我们首先要做的就是进入ghc的交互模式,接着就可以调几个函数小体验一把haskell了。打开控制台,输入ghci,你会看到如下欢迎信息
GHCi, version 6.8.2: http://www.haskell.org/ghc/
:? for help Loading package base ... linking ... done.
Prelude>
恭喜,您已经进入了ghci!目前它的命令行提示是prelude>,不过它在你装载什么东西后会变的比较长。免得碍眼,我们输入个:set prompt "ghci> "把它改成ghci>。
如下是一些简单的运算
ghci> 2 + 15 17
ghci> 49 * 100 4900
ghci> 1892 - 1472 420
ghci> 5 / 2 2.5
ghci>
很简单。也可以在一行中使用多个运算符,按照运算符优先级执行计算,使用括号可以更改优先级次序。
ghci> (50 * 100) - 4999
1
ghci> 50 * 100 - 4999
1
ghci> 50 * (100 - 4999)
-244950
很酷么?嗯,我承认不。处理负数时会有个小陷阱:执行5 * -3会使ghci报错。所以说,使用负数时最好将其置于括号之中,像5*(-3)就不会有问题。
逻辑运算也同样直白,你也许知道,&&指逻辑与,||指逻辑或,not指逻辑否。
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"或者5==True会怎样?好的,一个大大的报错等着你。
No instance for (Num [Char])
arising from a use of `+' at :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"
Yikes!ghci 提示说"llama"并不是数值类型,所以它不知道该怎样才能给它加上5。即便是“four”甚至是“4”也不可以,haskel不拿它当数值。执行True==5, ghci就会提示类型不匹配。+运算符要求两端都是数值,而==运算符仅对两个可比较的值可用。这就要求他们的类型都必须一致,苹果和橙子就无法做比较。我们会在后面深入地理解类型的概念。Note:5+4.0是可以执行的,5既可以做被看做整数也可以被看做浮点数,但4.0则不能被看做整数。
也许你并未察觉,不过从始至终我们一直都在使用函数。*就是一个将两个数相乘的函数,就像三明治一样,用两个参数将它夹在中央,这被称作中缀函数。而其他大多数不能与数夹在一起的函数则被称作前缀函数。绝大部分函数都是前缀函数,在接下来我们就不多做甄别。大多数命令式编程语言中的函数调用形式通常就是函数名,括号,由逗号分隔的参数表。而在haskell中,函数调用的形式是函数名,空格,空格分隔的参数表。简单据个例子,我们调用haskell中最无聊的函数:
ghci> succ 8
9
succ函数返回一个数的后继(successor, 在这里就是8后面那个数,也就是9。译者注)。如你所见,通过空格将函数与参数分隔。调用多个参数的函数也是同样容易,min和max接受两个可比较大小的参数,并返回较大或者较小的那个数。
ghci> min 9 10
9
ghci> min 3.4 3.2
3.2
ghci> max 100 101
101
函数调用拥有最高的优先级,如下两句是等效的
ghci> succ 9 + max 5 4 + 1
16
ghci> (succ 9) + (max 5 4) + 1
16
若要取9乘10的后继,succ 9*10是不行的,程序会先取9的后继,然后再乘以10得100。正确的写法应该是succ(9*10),得91。如果某函数有两个参数,也可以用 .. 符号将它括起,以中缀函数的形式调用它。例如取两个整数相除所得商的div函数,div 92 10可得9,但这种形式不容易理解:究竟是哪个数是除数,哪个数被除?使用中缀函数的形式 92div10 就更清晰了。从命令式编程走过来的人们往往会觉得函数调用与括号密不可分,在C中,调用函数必加括号,就像foo(),bar(1),或者baz(3,"haha")。而在haskell中,函数的调用必使用空格,例如bar (bar 3),它并不表示以bar和3两个参数去调用bar,而是以bar 3所得的结果作为参数去调用bar。在C中,就相当于bar(bar(3))`。
启蒙:你的第一个函数
在前一节中我们简单介绍了函数的调用,现在让我们编写我们自己的函数!打开你最喜欢的编辑器,输入如下代码,它的功能就是将一个数字乘以2.
doubleMe x = x + x
函数的声明与它的调用形式大体相同,都是先函数名,后跟由空格分隔的参数表。但在声明中一定要在 = 后面定义函数的行为。
保存为baby.hs或任意名称,然后转至保存的位置,打开ghci,执行:l baby.hs。这样我们的函数就装载成功,可以调用了。
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
+运算符对整数和浮点都可用(实际上所有有数字特征的值都可以),所以我们的函数可以处理一切数值。声明一个包含两个参数的函数如下:
doubleUs x y = x*2 + y*2
很简单。将其写成doubleUs x y = x + x + y + y也可以。测试一下(记住要保存为baby.hs并到ghci下边执行:l baby.hs)
ghci> doubleUs 4 9 26
ghci> doubleUs 2.3 34.2 73.0
ghci> doubleUs 28 88 + doubleMe 123
478
你可以在其他函数中调用你编写的函数,如此一来我们可以将doubleMe函数改为:
doubleUs x y = doubleMe x + doubleMe y
这种情形在haskell下边十分常见:编写一些简单的函数,然后将其组合,形成一个较为复杂的函数,这样可以减少重复工作。设想若是哪天有个数学家验证说2应该是3,我们只需要将doubleMe改为x+x+x即可,由于doubleUs调用到doubleMe,于是整个程序便进入了2即是3的古怪世界。
haskell中的函数并没有顺序,所以先声明doubleUs还是先声明doubleMe都是同样的。如下,我们编写一个函数,它将小于100的数都乘以2,因为大于100的数都已经足够大了!
doubleSmallNumber x