很久之前看到朋友们讨论: 函数式编程,柯里函数,类型类,函子,Monoid…一堆酷炫的东西,心神往之,早已埋下了好奇的种子,所以趁着国庆假期就买了本《Haskell趣学指南》,试图一窥函数式编程的面貌。写这篇博客一方面是让我自己对Haskell 更熟练,另一方面是希望能够分享我的学习经验,帮助初学者更快进入状况。

如果你也想简单的了解一下Haskell,这里还有该书的web版本《Haskell趣学指南》(我还是喜欢纸质书的写写画画~)。

从零开始

通过apt-get install Haskell-platform命令即可在Ubuntu下安装Haskell的GHC版本编译器。GHC可以解释执行.hs结尾的Haskell Script,ghci命令进行交互试模式,输入:l myfunctions.hs即可执行对应的myfunctions.hs文件。

1
GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
2
Prelude>

haskell会按照运算中优先级执行计算,也支持使用括号改变计算的优先级。

1
Prelude> 2 + 15
2
17
3
Prelude> 49 * 10.1
4
494.9
5
Prelude> 1892 - 1982
6
-90
7
Prelude> 5 / 2.0
8
2.5
9
Prelude> 50 * 100 - 4999
10
1
11
Prelude> (50 * 100) - 4999
12
1
13
Prelude> 50 * (100 - 4999)
14
-244950

处理负数时存在一个小陷阱:执行例如5 * -3,5 - -3,5 + -3,5 / -3等运算子后边的操作数为负数的运算时会报错,需要将后边的负数置于括号中,例如5 * (-3)就不会有问题。

布尔代数(Boolean Algebra)运算符为:&&,||,not,分别指代AND,OR与取反操作。

1
Prelude> True && False
2
False
3
Prelude> True && True
4
True
5
Prelude> False || True
6
True
7
Prelude> not False
8
True
9
Prelude> not (True && True)
10
False

相等性判断:

1
Prelude> 5 == 5
2
True
3
Prelude> 1 == 0
4
False
5
Prelude>  5/=5
6
False
7
Prelude> 5/=4
8
True
9
Prelude> "hello" == "hello"
10
True

函数

函数调用

haskell中函数有中缀函数与前缀函数两种形式,前缀函数调用形式为:函数名 空格 空格分隔的参数列表。两个参数的函数,可以用中缀函数形式调用:

1
Prelude> div 92 10
2
9
3
Prelude> 92 `div` 10
4
9

一些基础函数举例,succ函数返回一个数的后继,函数调用拥有最高优先级。

1
Prelude> succ 9 + max 5 4 + 1
2
16
3
Prelude> (succ 9) + (max 5 4) + 1
4
16

函数声明

函数的声明与它的调用形式大致相同,都是先函数名,后跟由空格分隔的参数表。但在声明中一定要在=后面定义函数的行为。

1
doubleMe x = x + x

也可以在其他函数中调用你编写的函数:

1
doubleUs x y = doubleMe x + doubleMe y

if语句

Haskell中if是表达式,if语句的else部分是不可省略。

1
Prelude> doubleSome x = if x > 100 then x else x * 2
2
Prelude> doubleSome' x = (if x > 100 then x else x * 2) + 1
3
Prelude> doubleSome' 12
4
25
5
Prelude> doubleSome 12
6
24

函数名最后的那个单引号,它没有任何特殊含义,只是一个函数名的合法字符罢了。通常,我们使用单引号来区分一个稍经修改但差别不大的函数。

List

Haskell中,List是一种单型别的数据结构,可以用来存储多个类型相同的元素。

1
Prelude> lista = [1,2,3,41,53,62]
2
Prelude> lista
3
[1,2,3,41,53,62]

字串实际上就是一组字符的List,”Hello”只是['h','e','l','l','o']的语法糖。我们可以使用处理List的函数来对字串进行操作。 例如:将两个List合并可以通过++运算子实现。

使用++运算子处理长字串时要格外小心(对长List也是同样),Haskell会遍历整个List(++符号左边的那个)。

1
Prelude> [1,2,3,4] ++ [1,3,5,7,9]
2
[1,2,3,4,1,3,5,7,9]
3
Prelude> "hello" ++ " " ++ "world"
4
"hello world"
5
Prelude> ['c','o','o'] ++ ['o','o','l']
6
"cooool"

:运算子连接一个元素到一个List或者字串之中,而++运算子则是连接两个List。[ ]表示一个空 List,[1,2,3]实际上是1:2:3:[]的语法糖。

1
Prelude> 'A' : " Boy"
2
"A Boy"
3
Prelude> 5:[1,2,3]
4
[5,1,2,3]

!!运算子,按照索引取得List中的元素,从0开始。试图在一个只含有 4 个元素的 List 中取它的第 6 个元素,就会报错。

1
Prelude> "Hello World"!!0
2
'H'
3
Prelude> [1,3,5,7,9]!!3
4
7

List同样也可以用来装List,甚至是List的List的List。(233

1
Prelude> let b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
2
Prelude> b
3
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
4
Prelude> b ++ [[1,1,1,1]]
5
[[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]]
6
Prelude> [6,6,6]:b
7
[[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]]
8
Prelude> b !! 2
9
[1,2,2,3,4]

List中的List可以是不同长度,但必须得是相同的型别。List内装有可比较的元素时,使用>>=可以比较List的大小。它会先比较第一个元素,若它们的值相等,则比较下一个,以此类推。

1
Prelude> [3,2,1] > [2,1,0]
2
True
3
Prelude> [3,2,1] > [2,10,100]
4
True
5
Prelude> [3,4,2] > [3,4]
6
True
7
Prelude> [3,4,2] > [2,4]
8
True
9
Prelude> [3,4,2] == [3,4,2]
10
True

List常用函数

函数名 说明
head 返回一个List的头部,也就是List的首个元素。
tail 返回一个List的尾部,也就是List除去头部之后的部分。
last 返回一个 List 的最后一个元素。
init 返回一个 List 除去最后一个元素的部分。
length 返回一个 List 的长度。
null 检查一个 List 是否为空。如果是,则返回 True,否则返回 False。应当避免使用 xs==[] 之类的语句来判断 List 是否为空,使用 null 会更好。
reverse 将一个 List 反转
take 返回一个 List 的前几个元素
drop 与 take 的用法大体相同,它会删除一个 List 中的前几个元素。
maximum 返回一个 List 中最大的那个元素。
minimun 返回最小的。
sum 返回一个 List 中所有元素的和。
product 返回一个 List 中所有元素的积。
elem 判断一个元素是否包含于一个 List,通常以中缀函数的形式调用它。
cycle 接受一个List做参数并返回一个无限List。
repeat 接受一个值做参数并返回一个无限List。
1
Prelude> head [5,4,3,2,1]
2
5
3
Prelude> tail [5,4,3,2,1]
4
[4,3,2,1]
5
Prelude> last [5,4,3,2,1]
6
1
7
Prelude> init [5,4,3,2,1]
8
[5,4,3,2]
9
Prelude> length [5,4,3,2,1]
10
5
11
Prelude> null [1,2,3]
12
False
13
Prelude> null []
14
True
15
Prelude> reverse [5,4,3,2,1]
16
[1,2,3,4,5]
17
Prelude> take 3 [5,4,3,2,1]
18
[5,4,3]
19
Prelude> take 5 [1,2]
20
[1,2]
21
Prelude> take 0 [6,6,6]
22
[]
23
Prelude> drop 3 [8,4,2,1,5,6]
24
[1,5,6]
25
Prelude> drop 0 [1,2,3,4]
26
[1,2,3,4]
27
Prelude> drop 100 [1,2,3,4]
28
[]
29
Prelude> minimum [8,4,2,1,5,6]
30
1
31
Prelude> maximum [1,9,2,3,4]
32
9
33
Prelude> sum [5,2,1,6,3,2,5,7]
34
31
35
Prelude> product [6,2,1,2]
36
24
37
Prelude> product [1,2,5,6,7,9,2,0]
38
0
39
Prelude> 4 `elem` [3,4,5,6]
40
True
41
Prelude> 10 `elem` [3,4,5,6]
42
False

Range构造方法

Range是构造List方法之一,而其中的值必须是可枚举的,像 1、2、3、4…字符同样也可以枚举,字母表就是 A..Z 所有字符的枚举。

1
Prelude> [1..20]
2
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
3
Prelude> ['a'..'z']
4
"abcdefghijklmnopqrstuvwxyz"
5
Prelude> ['K'..'Z']
6
"KLMNOPQRSTUVWXYZ"

Range的特点是他还允许你指定每一步该跨多远。

1
Prelude> [2,4..20]
2
[2,4,6,8,10,12,14,16,18,20]
3
Prelude> [3,6..20]
4
[3,6,9,12,15,18]

也可以不标明Range的上限,从而得到一个无限长度的 List。由于Haskell是惰性的,它不会对无限长度的List求值,它会等着,看你会从它那儿取多少。

1
Prelude> take 10 (cycle [1,2,3])
2
[1,2,3,1,2,3,1,2,3,1]
3
Prelude> take 12 (cycle "LOL ")
4
"LOL LOL LOL "
5
Prelude> take 10 (repeat 5)
6
[5,5,5,5,5,5,5,5,5,5]

List Comprehension

数学集合中的comprehension(Set Comprehension)指:可以从既有的集合中按照规则产生一个新集合。

例如前十个偶数的set comprehension可以表示为:$S = \lbrace 2 \cdot x \mid x \in N, x \leq 10 \rbrace$,竖线左端的部分是输出函数,x是变量,N是输入集合。

Haskell中可以用list comprehension来表示:

1
Prelude> [x*2 | x <- [1..10]]
2
[2,4,6,8,10,12,14,16,18,20]

给这个comprehension再添个*限制条件 *(predicate),它与前面的条件由一个逗号分隔。

例如:我们要求只取乘以2后大于等于12的元素:

1
Prelude> [x*2 | x <- [1..10], x*2 >= 12]
2
[12,14,16,18,20]

取50到100间所有除7的余数为3的元素:

1
Prelude> [x | x <- [50..100], x `mod` 7 == 3]
2
[52,59,66,73,80,87,94]

从一个List中筛选出符合特定限制条件的操作也可以称为过滤 (filtering)。即取一组数并且按照一定的限制条件过滤它们。

例如:我们想要一个comprehension,它能够使List中所有大于10的奇数变为”BANG”,小于10的奇数变为”BOOM”,其他则统统扔掉:

1
Prelude> boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]
2
Prelude> boomBangs [7..13]
3
["BOOM!","BOOM!","BANG!","BANG!"]

也可以加多个限制条件。若要达到 10 到 20 间所有不等于 13,15 或 19 的数,可以这样:

1
Prelude> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19]
2
[10,11,12,14,16,17,18,20]

除了多个限制条件之外,从多个 List 中取元素也是可以的。这样的话comprehension会把所有的元素组合交付给我们的输出函数。

1
Prelude> [ x*y | x <- [2,5,10], y <- [8,10,11]]
2
[16,20,22,40,50,55,80,100,110]
3
Prelude> [ x*y | x <-[2,5,10], y <- [8,10,11], x*y > 50]
4
[55,80,100,110]
5
Prelude> let nouns = ["hobo","frog","pope"]
6
Prelude> let adjectives = ["lazy","grouchy","scheming"]
7
Prelude> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]
8
["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog",
9
"grouchy pope","scheming hobo","scheming frog","scheming pope"]

自定义length函数,_表示我们并不关心从List中取什么值,这个函数将一个List中所有元素置换为 1,并且使其相加求和。得到的结果便是我们的List长度。

1
length' xs = sum [1 | _ <- xs]

除去字串中所有非大写字母的函数:

1
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

若操作含有List的List,使用嵌套的List comprehension也是可以的。假设有个包含许多数值的List的 List,让我们在不拆开它的前提下除去其中的所有奇数:

1
Prelude> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],
2
[1,2,4,2,1,6,3,1,3,2,3,6]]
3
Prelude> [ [ x | x <- xs, even x ] | xs <- xxs]
4
[[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]

Tuple(元组)

Tuple对需要组合的数据的数目非常的明确,它的型别取决于其中项的数目与其各自的型别。Tuple中的项由括号括起,并由逗号隔开。Tuple中的项不必为同一型别,在Tuple里可以存入多态别项的组合。

Tuple支持的操作

函数名 说明
fst 返回一个序对的首项
snd 返回序对的尾项
zip 用来生成一组序对(Pair)的list
1
Prelude> fst (8,11)
2
8
3
Prelude> snd (8,11)
4
11
5
Prelude> zip [1,2,3,4,5] [5,5,5,5,5]
6
[(1,5),(2,5),(3,5),(4,5),(5,5)]
7
Prelude> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
8
[(5,"im"),(3,"a"),(2,"turtle")]
9
Prelude> zip [1..] ["apple", "orange", "cherry", "mango"]
10
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

一个同时应用到 List 和 Tuple 的问题:如何取得所有三边长度皆为整数且小于等于 10,周长为 24 的直角三角形?

1
Prelude> let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b],
2
a^2 + b^2 == c^2, a+b+c == 24]
3
Prelude> rightTriangles'
4
[(6,8,10)]

Types and Typeclasses

Haskell是Static Type,这表示在编译时期每个表达式的型别都已经确定下来,这提高了代码的安全性。Haskell支持型别推导。

使用:t命令后跟任何可用的表达式,即可得到该表达式的型别。:t命令处理一个表达式的输出结果为表达式后跟::及其型别,::读作”它的型别为”。凡是明确的型别,其首字母必为大写

1
Prelude> :t 'a'
2
'a' :: Char
3
Prelude> :t True
4
True :: Bool
5
Prelude> :t "hello!"
6
"hello!" :: [Char]
7
Prelude> :t (True, 'a')
8
(True, 'a') :: (Bool, Char)
9
Prelude> :t 4 == 5
10
4 == 5 :: Bool

Type variables

同样,函数也有型别。

1
Prelude> :t head
2
head :: [a] -> a

之前说过,凡是型别其首字母必大写,所以a不是一个型别,它是个型别变量,意味着a可以是任意的型别。这一点与其他语言中的泛型(generic)很相似,但在Haskell中要更为强大。它可以让我们轻而易举地写出型别无关的函数。使用到型别变量的函数被称作”多态函数 “。

Typeclasses入门

型别定义行为的接口,如果一个型别属于某 Typeclass,那它必实现了该 Typeclass 所描述的行为。

1
Prelude> :t (==)
2
(==) :: Eq a => a -> a -> Bool

=>符号,它左边的部分叫做型别约束,”相等函数取两个相同型别的值作为参数并回传一个布林值,而这两个参数的型别同在Eq类之中(即型别约束)”。

几个基本的Typeclass:

typeclass 说明
Eq 可判断相等性的型别
Ord 可比较大小的型别
Show 可用字符串表示的型别
Read 与Show相反的Typeclass
Enum 可枚举的型别
Bounded 该型别都是有上限与下限的
Num 表示数字的Typeclass
Integral Num包含所有的数字:实数和整数。而Integral仅包含整数,其中的成员型别有Int和Integer
Floating 包含浮点型别:Float和Double

函数语法

模式匹配(Pattern matching)

模式匹配通过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据。

Guards