Haskell语言学习笔记

学习Haskell函数式编程语言,第一部分,基础知识和基本函数语法。

1 基础知识

1.1 语法清单

  • let a = 7:let 用于定义常量,常量名、函数名必须首字母小写

1.2 List、德州区间和 List Comprehension

1.2.1 List

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

1
2
3
ghci> let lostNumbers = [4,8,15,16,23,48]   
ghci> lostNumbers
[4,8,15,16,23,48]

如上,一个List由方括号括起,其中的元素用逗号分隔开来。

  • "hello" ++ " " ++ "world" 得到 "hello world":++运算符,用于连接两个List

  • 5:[1,2,3,4,5] 得到 [5,1,2,3,4,5]:使用:运算符往一个List前端插入元素

  • "Steve Buscemi" !! 6 得到 B:使用!!运算符按照索引取得List中的元素

  • 用> < >= = 等符号进行List间的比较
  • 其余常用函数参见 第二章 Haskell入门_w3cschool

1.2.2 德州区间

区间(Range)是构造 List 方法之一,而其中的值必须是可枚举的,如1、2、3,A、B、C等。

1
2
3
4
5
6
7
8
9
10
11
ghci> [1..20]   --简单的区间
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

ghci> [2,4..20] --带步长的区间
[2,4,6,8,10,12,14,16,18,20]

ghci> take 10 (cycle [1,2,3]) --cycle用于循环单个List
[1,2,3,1,2,3,1,2,3,1]

ghci> take 10 (repeat 5) --repeat用于循环单个值
[5,5,5,5,5,5,5,5,5,5]

1.2.3 List Comprehension

List Comprehension能够更加方便自由地生成List,类似于集合的描述法:

1
2
3
4
5
6
7
8
ghci> [x*2 | x <- [1..10]]   --前10个整数
[2,4,6,8,10,12,14,16,18,20]

ghci> [ x | x <- [50..100], x `mod` 7 == 3] --取50到100间所有除7的余数为3的元素
[52,59,66,73,80,87,94]

ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]] --从多个List中取元素
[16,20,22,40,50,55,80,100,110]

1.3 Tuple

Tuple (元组)要求你对需要组合的数据的数目非常的明确,它的类型取决于其中项的数目与其各自的类型。 Tuple 中的项由括号括起,并由逗号隔开。另外,Tuple 中的项不必为同一类型,在 Tuple 里可以存入多类型项的组合。

1
2
3
4
5
6
7
8
9
10
ghci> fst (8,11)   --返回一个序对的首项
8

ghci> snd (8,11) --返回一个序对的尾项
11

-- 上述两个函数仅对序对有效,而不能应用于三元组,四元组和五元组之上。

ghci> zip [1,2,3,4,5] [5,5,5,5,5] --zip 生成一组序对的List
[(1,5),(2,5),(3,5),(4,5),(5,5)]

1.4 类型和类型类

  • 第三章 Haskell类型和类型类_w3cschool

  • Haskell 的类型必须是首字母大写

  • 使用:t命令后跟任何可用的表达式,可以检测表达式的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ghci> :t 'a'   
    'a' :: Char
    ghci> :t True
    True :: Bool
    ghci> :t "HELLO!"
    "HELLO!" :: [Char] --[char] <=> String
    ghci> :t (True, 'a')
    (True, 'a') :: (Bool, Char)
    ghci> :t 4 == 5
    4 == 5 :: Bool
  • 定义函数时,可以定义参数和返回值类型,参数之间以及参数和返回值之间均使用->分隔

    1
    2
    addThree :: Int -> Int -> Int -> Int   --三个参数,一个返回值
    addThree x y z = x + y + z

2 函数语法

2.1 函数声明方法

  • 函数声明:先函数名,后跟由空格分隔的参数表。声明中一定要在=后面定义函数的行为

    1
    doubleMe x = x + x  --功能:将一个数字乘以2
  • 可以在其他函数中调用自己编写的函数,不用考虑函数出现的先后顺序

    1
    doubleUs x y = doubleMe x + doubleMe y  --功能:接收两个参数,返回它们的和的2倍
  • 为函数编写明确的类型声明是一个好习惯

    1
    2
    3
    4
    5
    6
    7
    --功能:过滤大写字母
    removeNonUppercase :: [Char] -> [Char]
    removeNonUppercase st = [ c | c st, c `elem` ['A'..'Z']]

    --功能:三个整数相加
    addThree :: Int -> Int -> Int -> Int
    addThree x y z = x + y + z

2.2 模式匹配

模式匹配通过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据。类似于switch...case...结构。

1
2
3
4
5
6
7
8
9
sayMe :: (Integral a) => a -> String   
sayMe 1 = "One!" --这里 “1” 是参数
sayMe 2 = "Two!"
sayMe 3 = "Three!"
sayMe 4 = "Four!"
sayMe 5 = "Five!"
sayMe x = "Not between 1 and 5"

--如果把最后匹配一切的那个模式挪到最前,它的结果就全都是"Not between 1 and 5"

注意

  • 不可以在模式匹配中使用 ++

2.2.1 对Tuple使用模式匹配

1
2
3
4
5
6
7
--不会模式匹配的时候
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)

--一个更好的方法
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

用模式匹配实现针对三元组的first、second、third函数

1
2
3
4
5
6
7
8
9
first :: (a, b, c) -> a   
first (x, _, _) = x
--"_"表示我们不关心这部分的具体内容

second :: (a, b, c) -> b
second (_, y, _) = y

third :: (a, b, c) -> c
third (_, _, z) = z

2.2.2 在List Comprehension中使用模式匹配

1
2
3
ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]   
ghci> [a+b | (a,b) xs]
[4,7,6,8,11,4]

一旦模式匹配失败,它就简单挪到下个元素。

2.2.3 对List使用模式匹配

[]:来匹配List。例如:

  • x:xs模式,可以将list的头部绑定为x,尾部绑定为xs,但这种模式只能匹配长度大于等于1的List,因此对于空的List需要进行特殊判断。
  • x:y:z:xs模式,可以将List的前三个元素都绑定到变量中,但只能匹配长度大于等于3的List,因此对于长度小于3的List需要进行特殊判断。
1
2
3
4
--实现一个自己的 head 函数
head' :: [a] -> a
head' [] = error "Can't call head on an empty list, dummy!"
head' (x:_) = x

2.2.4 as模式

所谓as模式,就是将一个名字和@置于模式前,可以在按模式分割什么东西时仍保留对其整体的引用。如这个模式xs@(x:y:ys),它会匹配出与x:y:ys对应的东西,同时你也可以方便地通过xs得到整个list,而不必在函数体中重复x:y:ys

1
2
3
4
5
6
7
8
--代码--
capital :: String -> String
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

--执行结果--
ghci> capital "Dracula"
"The first letter of Dracula is D"

2.3 门卫

1
2
3
4
5
6
7
--一个用到了门卫的函数--
bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
  • 门卫由跟在函数名及参数后面的竖线标志,通常他们都是靠右一个缩进排成一列。一个门卫就是一个布尔表达式,如果为真,就使用其对应的函数体。如果为假,就送去见下一个门卫,如之继续。

  • 如果一个函数的所有门卫都没有通过(而且没有提供otherwise作万能匹配),就转入下一模式。这便是门卫与模式契合的地方。如果始终没有找到合适的门卫或模式,就会发生一个错误。

2.4 where绑定

1
2
3
4
5
6
7
8
--使用where关键字从而避免重复工作
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
  • where关键字跟在门卫后面(最好是与竖线缩进一致)。
  • 可以定义多个名字和函数,这些名字对每个门卫都是可见的。
  • 函数在where绑定中定义的名字只对本函数可见,因此我们不必担心它会污染其他函数的命名空间。
  • where绑定不会在多个模式中共享。如果你在一个函数的多个模式中重复用到同一名字,就应该把它置于全局定义之中。

  • where绑定也可以使用模式匹配

    1
    2
    3
    ...   
    where bmi = weight / height ^ 2
    (skinny, normal, fat) = (18.5, 25.0, 30.0)

2.5 let绑定

1
2
3
4
5
6
--依据半径和高度求圆柱体表面积--
cylinder :: (RealFloat a) => a -> a -> a
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^2
in sideArea + 2 * topArea

let的格式为let [bindings] in [expressions]。在let中绑定的名字仅对in部分可见。let里面定义的名字也得对齐到一列。

note:let绑定和where绑定的区别:

**(1)where绑定是个语法结构,let绑定是个表达式,可以随处安放(就像if语句一样)。** **(2)let定义域限制的相当小,因此不能在多个门卫中使用。where跟在函数体后面,主函数体距离类型声明近一些,会更易读。**
  • let可以定义局部函数

    1
    2
    ghci> [let square x = x * x in (square 5, square 3, square 2)]  
    [(25,9,4)]
  • 若要在一行中绑定多个名字,可以用分号将其分开

    1
    2
    ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)  
    (6000000,"Hey there!")
  • 可以在let绑定中使用模式匹配。这在从Tuple取值之类的操作中很方便。

    1
    2
    ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100  
    600
  • 也可以把let绑定放到List Comprehension中。我们重写下那个计算bmi值的函数,用个let替换掉原先的where。

    1
    2
    calcBmis :: (RealFloat a) => [(a, a)] -> [a]  
    calcBmis xs = [bmi | (w, h) xs, let bmi = w / h ^ 2]

2.6 case表达式

模式匹配本质上不过就是case语句的语法糖而已。这两段代码就是完全等价的:

1
2
3
head' :: [a] -> a  
head' [] = error "No head for empty lists!"
head' (x:_) = x
1
2
3
head' :: [a] -> a   
head' xs = case xs of [] -> error "No head for empty lists!"
(x:_) -> x

case表达式的语法:

1
2
3
4
case expression of pattern -> result   
pattern -> result
pattern -> result
...

note:case表达式和模式匹配的区别:

函数参数的模式匹配只能在定义函数时使用,而case表达式可以用在任何地方

3 关于函数式编程和Haskell的参考文章

Clojure和Haskell——深度学习中的函数式语言之美-InfoQ

学习Haskell的现实意义-InfoQ

上学期看崔毅东老师的C++课程时,依稀记得老师提到了这门语言,于是大概学习了一下,第一次接触函数式编程的思想,蛮有趣但还是比较懵,之后再找找实战玩玩看。