8

Haskell趣学指南

 3 years ago
source link: https://atticuslab.com/2020/10/05/haskell-learning/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Haskell趣学指南

2020-10-05

37

haskell_logo.png

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

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

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

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

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

Prelude> 2 + 15
2
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
Prelude> (50 * 100) - 4999
Prelude> 50 * (100 - 4999)
-244950

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

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

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)
False

相等性判断:

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"
True

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

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

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

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

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

doubleMe x = x + x

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

doubleUs x y = doubleMe x + doubleMe y

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

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

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

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

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(++符号左边的那个)。

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:[]的语法糖。

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

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

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

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

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的大小。它会先比较第一个元素,若它们的值相等,则比较下一个,以此类推。

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]
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。
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
7
Prelude> init [5,4,3,2,1]
8
[5,4,3,2]
9
Prelude> length [5,4,3,2,1]
5
Prelude> null [1,2,3]
False
Prelude> null []
True
Prelude> reverse [5,4,3,2,1]
[1,2,3,4,5]
Prelude> take 3 [5,4,3,2,1]
[5,4,3]
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
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 所有字符的枚举。

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的特点是他还允许你指定每一步该跨多远。

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求值,它会等着,看你会从它那儿取多少。

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={2⋅x∣x∈N,x≤10}S={2⋅x∣x∈N,x≤10},竖线左端的部分是输出函数,x是变量,N是输入集合。

Haskell中可以用list comprehension来表示:

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

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

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

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

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

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”,其他则统统扔掉:

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 的数,可以这样:

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

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

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长度。

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

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

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

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

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
Prelude> fst (8,11)
2
8
3
Prelude> snd (8,11)
4
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"]
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

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

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命令处理一个表达式的输出结果为表达式后跟::及其型别,::读作”它的型别为”。凡是明确的型别,其首字母必为大写

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
4 == 5 :: Bool

Type variables

同样,函数也有型别。

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

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

Typeclasses入门

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK