很久之前看到朋友们讨论: 函数式编程,柯里函数,类型类,函子,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)
模式匹配通过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据。