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