haskell入门
运算
大部分的运算符和Java/C语言类似,注意三个点
- 使用 /= 来表示不等,而不是 !=
- 使用 not 来表示非,而不是 !
- 使用负数时最好将其置于括号之中,比如 5(-3)* ,会比5*-3 更好
关于运算符的类型支持:
比如 5+a 这样的运算是不允许的,因为 a 不是一个数值类型的数据
在haskell中, + 只能用于数值类型的数据, = 只能用于相同类型的数据
haskell的函数
函数是haskell的核心
常见的函数包括两种:
- 前缀函数,比如 succ 8 , min 9 10 在函数名后面跟上参数
- 中缀函数,比如 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 | div 92 10 |
两者是等价的
函数
函数是haskell中的一等公民
函数的定义
编写一个函数的格式是
1 | 函数名 参数1 参数2 ... = 函数体 |
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 | doubleMe x = x + x |
List
List是haskell中最常用的数据结构,List中的元素必须是同一类型的
List的定义
在haskell中,List的定义格式是
1 | [元素1,元素2,...] |
比如
1 | [1,2,3,4,5] |
同一个List中的元素必须是同一类型的
实际上,haskell中的字符串实际就是一个字符的List
1 | "hello" == ['h','e','l','l','o'] |
因此,在haskell中可以使用List的函数来操作字符串,常用的有 ++ 和 :,
前者用于连接两个List,后者用于将一个元素添加到一个List的头部
1 | [1,2,3,4] ++ [9,10,11,12] -- [1,2,3,4,9,10,11,12] |
List的索引是从0开始的
可以使用 !! 来获取List中的元素
1 | "hello" !! 0 -- 'h' |
List的比较
haskell中的List是可以比较的,比较的规则是,先比较第一个元素,如果相等,则比较第二个元素,以此类推
1 | [3,2,1] > [2,1,0] -- 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 | null [] -- True |
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 | [1..20] -- [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] |
Range支持自动推断步长,在书写时,只需要指定前两个元素即可
1 | [1..] -- [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,...] |
如果不加以范围限制,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 | boomBangs xs = [if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x] |
我们将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 | [x*y | x <- [2,5,10], y <- [8,10,11]] |
这个功能可以用于将两个List中的元素进行组合,比如
1 | let nouns = ["apple","banana","orange"] |
List Comprehension和length函数
如何实现一个函数,用于计算一个List的长度?用List Comprehension可以很容易的实现
1 | length' xs = sum [1 | _ <- xs] |
在这里,我们使用了一个匿名变量 _ ,
这个变量表示,我们并不关心取出的元素是什么,只是为了计数而已
在haskell中,使用 _ 来表示一个匿名变量,这个变量的值是不会被使用的
List Comprehension和字符串
在haskell中,字符串实际上就是一个字符的List,因此,我们可以使用List Comprehension来操作字符串
1 | removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']] |
我们从字符串st中取出一个字符c,然后判断这个字符是否在[‘A’..’Z’]中,如果在,则将其添加到List中
从而实现了一个函数,用于将字符串中的所有小写字母去除
List Comprehension和嵌套List
就像List可以嵌套一样,List Comprehension也可以嵌套
1 | 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]] |
我们可以把他写得更加清晰一些
1 | [ [ x | x <- xs, even x ] | xs <- xxs] |
这样就很清晰了,我们首先从xxs中取出一个xs,然后从xs中取出一个x,然后判断x是否为偶数,如果是,则将其添加到List中
Tuple
和Python中的Tuple一样,haskell中的Tuple是用于存储多个不同类型的数据的,
用括号括起每一个项,项之间用逗号分隔
和List不同,Tuple中允许不同类型的数据混合在一起
1 | (1,2) -- (1,2) |
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 | fst (1,2) -- 1 |
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 | let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ] -- 生成所有三边长度皆为整数且小于等于10的三角形 |
而在Java/C中,我们可能会这样写
1 | for(int a = 1; a <= 10; a++) { |
这便是函数式编程的一般思路,声明式编程,而不是命令式编程