运算

大部分的运算符和Java/C语言类似,注意三个点

  1. 使用 /= 来表示不等,而不是 !=
  2. 使用 not 来表示非,而不是 !
  3. 使用负数时最好将其置于括号之中,比如 5(-3)* ,会比5*-3 更好

关于运算符的类型支持:
比如 5+a 这样的运算是不允许的,因为 a 不是一个数值类型的数据
在haskell中, + 只能用于数值类型的数据, = 只能用于相同类型的数据

haskell的函数

函数是haskell的核心
常见的函数包括两种:

  1. 前缀函数,比如 succ 8 , min 9 10 在函数名后面跟上参数
  2. 中缀函数,比如 9 `div` 2 , True && False 在参数中间使用函数名

在haskell中,函数的调用形式是 函数名 参数1 参数2 …
这与Java/C不同,haskell中函数的调用必须使用空格来分隔函数名和参数,而不是使用括号
函数作为一等公民有最高优先级,比如

1
succ 9 + max 5 4 + 1

的运算优先级是

1
(succ 9) + (max 5 4) + 1

所以结果是16而不是17,所以在使用函数时,要特别注意括号的使用。

可以使用 ` 来将函数转换为中缀函数,比如

1
2
div 92 10
92 `div` 10

两者是等价的

函数

函数是haskell中的一等公民

函数的定义

编写一个函数的格式是

1
2
3
4
5
6
7
8
9
10
11
12
函数名 参数1 参数2 ... = 函数体
```
比如
```haskell
doubleMe x = x + x
doubleUs x y = x*2 + y*2
```
当我们在一个hs文件中定义了一个函数后,可以在ghci中使用 **:l** 命令来加载这个文件,加载后就可以使用这个文件中的函数了

在haskell中,还可以将简单函数组合成复杂函数,比如
```haskell
doubleUs x y = doubleMe x + doubleMe y

haskell中的函数是没有顺序的,因此先声明的函数可以在后面的函数中使用。

if语句和函数返回

在haskell中,if语句的格式是

1
if 条件 then 表达式1 else 表达式2

和Java/C不同,haskell中的if语句是 必须要有else分支 的,
因为在haskell中,if语句是一个表达式而不是一个语句,表达式必须要有返回值,而语句则不需要,
比如 5 就是一个表达式,返回为5.
比如:

1
doubleSmallNumber x = if x > 100 then x else x*2

这个函数的作用是,如果参数大于100,则返回参数,否则返回参数的两倍

定义-没有参数的函数

在haskell中,定义一个没有参数的函数的格式是

1
函数名 = 表达式

这样的函数被称为定义

函数的命名规则

在haskell中,函数的命名规则是,函数名必须以小写字母开头,后面可以跟上任意数量的字母、数字、下划线
另外,通常用**’**来表示一个函数的修改版本,比如

1
2
doubleMe x = x + x
doubleMe' x = x + x + x

List

List是haskell中最常用的数据结构,List中的元素必须是同一类型的

List的定义

在haskell中,List的定义格式是

1
[元素1,元素2,...]

比如

1
2
[1,2,3,4,5]
['h','e','l','l','o']

同一个List中的元素必须是同一类型的
实际上,haskell中的字符串实际就是一个字符的List

1
"hello" == ['h','e','l','l','o']

因此,在haskell中可以使用List的函数来操作字符串,常用的有 ++:
前者用于连接
两个List
,后者用于将一个元素添加到一个List的头部

1
2
[1,2,3,4] ++ [9,10,11,12] -- [1,2,3,4,9,10,11,12]
'A':" SMALL CAT" -- "A SMALL CAT"

List的索引是从0开始的
可以使用 !! 来获取List中的元素

1
2
"hello" !! 0 -- 'h'
[1,2,3,4,5] !! 3 -- 4

List的比较

haskell中的List是可以比较的,比较的规则是,先比较第一个元素,如果相等,则比较第二个元素,以此类推

1
2
3
4
5
[3,2,1] > [2,1,0] -- True
[3,2,1] > [2,10,100] -- True
[3,4,2] > [3,4] -- True
[3,4,2] > [2,4] -- True
[3,4,2] == [3,4,2] -- True

如果是不同长度的List,且前面的元素都相等,则长度较长的List更大

1
[3,4,2] > [3,4,2,1] -- False

如果List的类型不同,则无法比较

1
[1,2,3] > "hello" -- 错误

List的常用函数

head 返回List的第一个元素

1
head [1,2,3,4,5] -- 1

tail 返回List除第一个元素外的所有元素

1
tail [1,2,3,4,5] -- [2,3,4,5]

last 返回List的最后一个元素

1
last [1,2,3,4,5] -- 5

init 返回List除最后一个元素外的所有元素

1
init [1,2,3,4,5] -- [1,2,3,4]

注意,以上四个函数都不能用于空List,否则会报错

length 返回List的长度

1
length [1,2,3,4,5] -- 5

null 判断List是否为空

1
2
null [] -- True
null [1,2,3] -- False

reverse 返回List的逆序

1
reverse [1,2,3,4,5] -- [5,4,3,2,1]

take 返回List的前n个元素,如果n大于List的长度,则直接返回整个List

1
take 3 [1,2,3,4,5] -- [1,2,3]

drop 返回List除前n个元素外的所有元素,如果n大于List的长度,则返回空List

1
drop 3 [1,2,3,4,5] -- [4,5]

maximum 返回List中的最大值

1
maximum [1,2,3,4,5] -- 5

minimum 返回List中的最小值

1
minimum [1,2,3,4,5] -- 1

sum 返回List中所有元素的和

1
sum [1,2,3,4,5] -- 15

product 返回List中所有元素的积

1
product [1,2,3,4,5] -- 120

elem 判断一个元素是否在List中,通常使用他的中缀形式 `elem`

1
4 `elem` [1,2,3,4,5] -- True

Range

在haskell中,可以使用 .. 也就是Range来生成一个范围,比如

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

Range支持自动推断步长,在书写时,只需要指定前两个元素即可

1
2
3
[1..] -- [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,...]
[1,3..] -- [1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,...]
[20,19..] -- [20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,...]

如果不加以范围限制,Range会一直生成下去,最终得到一个无限长的List,这在haskell中是允许的

1
[1..20] -- [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,...,20]

可以使用 take 来限制List的长度,使用take可以从一个无限长的List中取出前n个元素

1
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]

cycle 接受一个List作为参数,然后将这个List无限重复下去

1
take 10 (cycle [1,2,3]) -- [1,2,3,1,2,3,1,2,3,1]

repeat 接受一个元素作为参数,然后将这个元素无限重复下去

1
take 10 (repeat 5) -- [5,5,5,5,5,5,5,5,5,5]

如果,想要获取包含n个相同元素的List,可以使用 replicate

1
replicate 3 10 -- [10,10,10]

List Comprehension

列表推导式是一种生成List的方法,类似于数学中的集合推导式
例如想要获取1到10的所有偶数:

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

这里的 x <- [1..10] 表示从1到10中取出一个元素,然后将这个元素赋值给x,
然后将x*2作为一个元素添加到List中
在此基础之上,还可以继续添加条件,比如获取1到10中所有偶数,且这个偶数大于等于12

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

再比如,获取20到100中所有能被7整除的数

1
[x | x <- [20..100], x `mod` 7 == 0] -- [21,28,35,42,49,56,63,70,77,84,91,98]

List Comprehension的实际使用相当的灵活,
比如有需求:
对于所有小于10的奇数,将其转换为”BOOM!”,对于所有大于10的奇数,将其转换为”BANG!”,然后将所有的结果放到一个List中

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

我们将List Comprehension封装成了一个函数,在其当中,对于取出的每一个元素进行了if判断,这在haskell中是允许的

限制条件可以有多个,比如获取所有小于10的奇数,且这个奇数不等于7

1
[x | x <- [1..10], odd x, x /= 7] -- [1,3,5,9]

多个List的List Comprehension

List Comprehension可以使用多个List,比如

1
2
[x*y | x <- [2,5,10], y <- [8,10,11]] 
-- [16,20,22,40,50,55,80,100,110]

这个功能可以用于将两个List中的元素进行组合,比如

1
2
3
4
let nouns = ["apple","banana","orange"]
let adjectives = ["red","yellow","green"]
[adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]
-- ["red apple","red banana","red orange","yellow apple","yellow banana","yellow orange","green apple","green banana","green orange"]

List Comprehension和length函数

如何实现一个函数,用于计算一个List的长度?用List Comprehension可以很容易的实现

1
2
length' xs = sum [1 | _ <- xs]
length' [1,2,3,4,5] -- 5

在这里,我们使用了一个匿名变量 _
这个变量表示,我们并不关心取出的元素是什么,只是为了计数而已
在haskell中,使用 _ 来表示一个匿名变量,这个变量的值是不会被使用的

List Comprehension和字符串

在haskell中,字符串实际上就是一个字符的List,因此,我们可以使用List Comprehension来操作字符串

1
2
removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]
removeNonUppercase "Hello, World!" -- "HW"

我们从字符串st中取出一个字符c,然后判断这个字符是否在[‘A’..’Z’]中,如果在,则将其添加到List中
从而实现了一个函数,用于将字符串中的所有小写字母去除

List Comprehension和嵌套List

就像List可以嵌套一样,List Comprehension也可以嵌套

1
2
3
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]]
[ [ x | x <- xs, even x ] | xs <- xxs]
-- [[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]

我们可以把他写得更加清晰一些

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

这样就很清晰了,我们首先从xxs中取出一个xs,然后从xs中取出一个x,然后判断x是否为偶数,如果是,则将其添加到List中

Tuple

和Python中的Tuple一样,haskell中的Tuple是用于存储多个不同类型的数据的,
用括号括起每一个项,项之间用逗号分隔
和List不同,Tuple中允许不同类型的数据混合在一起

1
2
3
(1,2) -- (1,2)
(1,"hello") -- (1,"hello")
(1,2,"hello") -- (1,2,"hello")

Pair-特殊的Tuple

特别注意,一般情况下不使用除了Pair以外的Tuple

一个长度为2的Tuple被称为Pair,Pair在haskell中是一个独立的类型,
Pair的类型声明为 (a,b) ,其中a和b可以是任意类型,但是a和b的类型必须相同

考虑我们要表示一个平面坐标集,当然可以使用一个List来表示,比如

1
[ [1,2], [8,11], [4,5] ]

但是这样的表示方法并不好,因为对于这样的List,添加一个三元组也是可以的,比如

1
[ [1,2], [8,11], [4,5], [1,2,3] ]

因此,我们需要一个更好的表示方法

1
[ (1,2), (8,11), (4,5) ]

使用Tuple来表示平面坐标集,如果我们尝试将一个三元组添加到这个List中

1
[ (1,2), (8,11), (4,5), (1,2,3) ]

程序便会返回一个错误

使用Tuple前,需要先了解Tuple的类型,Tuple的类型是根据其长度和每个项的类型来确定的
因而,不可以用一个函数来给Tuple追加元素,比如

1
(1,2) ++ (3,4) -- 错误

fst和snd

fst和snd分别用于获取一个Pair的第一个元素和第二个元素

1
2
fst (1,2) -- 1
snd (1,2) -- 2

zip

zip接受两个List作为参数,
然后将这两个List中的元素一一对应,然后将这些对应的元素组成一个Tuple,最终返回一个List

1
zip [1,2,3,4,5] [5,5,5,5,5] -- [(1,5),(2,5),(3,5),(4,5),(5,5)]

如果两个List的长度不同,则zip会将较长的List截断为和较短的List一样的长度

1
zip [1,2,3,4,5] [5,5,5] -- [(1,5),(2,5),(3,5)]

时刻记住,haskell是惰性的,也就是无限长的List是被允许的

什么是函数式编程

有需求:取得所有三边长度皆为整数且小于等于10,周长为24的直角三角形

1
2
3
let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ] -- 生成所有三边长度皆为整数且小于等于10的三角形
let rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2 ] -- 生成所有三边长度皆为整数且小于等于10的直角三角形
let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24 ] -- 生成所有三边长度皆为整数且小于等于10,周长为24的直角三角形

而在Java/C中,我们可能会这样写

1
2
3
4
5
6
7
8
9
for(int a = 1; a <= 10; a++) {
for(int b = 1; b <= 10; b++) {
for(int c = 1; c <= 10; c++) {
if(a*a + b*b == c*c && a+b+c == 24) {
System.out.println(a + " " + b + " " + c);
}
}
}
}

这便是函数式编程的一般思路,声明式编程,而不是命令式编程