Mengqi's blog

Haskell 学习笔记 1:基本语法与类型类


Haskell 是一门静态类型的纯函数式编程语言,比较著名的就是它的类型系统、「纯函数」性、惰性求值。我个人觉得学习 Haskell 对学习理解其它编程语言(尤其是静态语言)很有帮助,比如 Java 中的泛型、JavaScript 中的 Promise 等等,因此在这里对自己的 Haskell 学习之旅进行一个总结。

相信大部分读者和我一样,都是从 C 系语言开始学起的,这里假设你和我一样具有一定的 C 系语言的编程基础,因此主要列举一些和 C 系语言不一样的地方。

基本语法

1. 前缀函数调用

函数都是以前缀的形式调用的,如:min 2 3 就是取函数 min 施加上两个参数 2 和 3。

有些函数以前缀形式不方便读,可以改用中缀形式表示,但要用 ``` 符号将函数包起来。

2. if

Haskell 中的 ifelse 部分不能省略。实际上,这里的 if 是像 [2, 3] 这样的「必然返回结果的表达式」,而非语句。

列表

3. 列表拼接

列表拼接使用 : 运算符将元素插入列表头部,如:

> 'A':' small cat'
A small cat

而常用的 [] 形式实际上是语法糖:[1,2,3]等同于1:2:3:[]。也就是说,列表不是「构造」出来的,而是通过组合施加 : 得出来的。

4. 列表的比较

只要列表内的元素可以比较,那么这两个列表就能作比较。具体做法是,两个列表各自从前向后挑一个元素出来做比较,如果不相等,则以两个元素的比较结果作为列表的比较结果,否则两个列表继续挑下一个比较,重复这个过程直到最后一个元素。所有元素都相等时,两个列表相等。

注意,非空列表总比空列表更大。

5. range

Haskell 中,列表可以只给出起始元素、第二个元素和上限,让 Haskell 自己推算出来中间元素,比如:

> [1,2..10]
[1,2,3,4,5,6,7,8,9,10]

当步长为 1 时,第二个元素可以省略,上面的式子可以简写为 [1..10]

需要注意的是,给出区间的上限并不一定是最后一个元素,比如:

> [1,3..10]
[1,3,5,7,9]

当 Haskell 推断到元素大于给定的上限时就不会再继续推断,因此上面的列表推断到 9 就截至了。

6. 无限列表

作为一个惰性求值的语言,Haskell 支持无限长列表,比如 [13, 26..] 是合法的,在 repl 中执行这个语句会让解释器不停地推断下去。( 感觉失去控制时可以按 Ctrl-C 刹车:) )

7. 列表推导式

可以像当初写数学的集合推导式一样声明一个列表,这个特性后来还被 Python 借鉴过去了。

> [x*2 | x <- [1..5]]
[2,4,6,8,10]
> [x*2 | x <- [1..5], x*2 < 8]
[2,4,6]

也可以声明两个列表的笛卡尔积:

> [x*y | x <- [1..3], y <- [2..4]]
[2,3,4,4,6,8,6,9,12]

类型系统

Haskell 作为一种静态类型的函数式编程语言,类型系统在 Haskell 中有举足轻重的地位。

类型声明

虽然 Haskell 具有类型推断的能力,但我们平时在编写 Haskell 函数时,最好还是显式地在函数声明上方声明它的类型。函数的类型声明就是要声明出这个函数的输入是什么类型、输出是什么类型,例如:

removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z'] ]
 
addTree :: Int -> Int -> Int -> Int
addTree x y z = x + y + z

注意如果函数有多个参数,则参数之间用 -> 分隔(参数和返回值之间同样也使用 -> 分隔,我们后面会介绍为什么这样做)

类型变量

类型变量类似 Java 等语言中的泛型,可以将类型声明中的类型泛化,不限制具体的类型,比如系统自带的函数 head

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

head 的类型声明表示了它可以接受装有任何元素类型的列表,并将返回这种元素类型的值(比如传入一个 Int 列表,将返回一个 Int 值)。

类型类

类型类(typeclass)类似于 Java 中的接口概念,是定义了一组行为的接口。一个类型(type)可以是一个类型类的实例(instance),但必须要实现这个类型类所定义的行为。

一个类型类的例子是定义相等性的 Eq。一个类型,只要它实现了 Eq 类型类,那么就可以通过 == 运算符判断这个类型不同值的相等性:

> :t (==)
(==) :: (Eq a) => a -> a -> Bool

上面的类型声明中,多了一个 => 记号,这个叫做类型约束,表示后面的类型变量 a 必须满足 Eq a 的约束,就是说 a 的类型必须实现了 Eq 这个类型类。可以类比于 Java 泛型中的 <? implements Eq>

为了方便接受,我们刚才都是用 Java 中相似的概念作类比的。然而类型类的概念和 Java 中的接口还是有很大不同的:Haskell 世界中函数是一等成员,任何 Haskell 函数都可以扩展类型类的行为,类型类在定义时没有声明它的行为。此外,Java 中接口在定义之时就要在接口内声明好它的行为,之后所有实现接口的类都无一例外地要实现所有行为。而在实际开发中可能用不到接口的所有函数,那些没有用到的函数有时候就通过 return null; 的方式过掉了,这样做其实会给之后的开发埋下隐患。

常见的几个类型类
Eq 类型类

Eq 用于判断相等性和不等性,所有 Eq 的实例类型都必须实现 ==/= (不等于)两个函数。

Ord 类型类

Ord 用于判断大小,所有 Ord 的实例类型都必须实现 <><=>= 等函数。

Show 类型类

Show 用于表示类型的字符串,show 函数可以取任一 Show 类型类的实例类型作为参数,返回一个表示参数值的字符串。比如:

> : t show
show :: (Show a) => a -> String
> show 5.334
"5.334"
Read 类型类

Read 类型类和 Show 正相反,read 函数可以将字符串转换为 Read 的某个实例类型:

> :t read
read :: (Read a) => String -> a
 
> read "8.2" + 3.8
12.0
 
> [read "True" True False]
[True, True, False]

要注意的是,read 函数需要有其它上下文(比如上面例子中的 3.8 和 [True False])才能推断出要转换到什么类型。只输入 read "4",haskell repl 会报错。要告诉解释器我们需要转换为什么类型,需要用到类型注解,比如:

> read "4" :: Int
4
> read "4" :: Float
4.0
Enum 类型类

Enum 类型类的实例都有连续顺序,它的实例类型都需要支持 succpred 函数获取每个值的后继和前驱,使得我们可以在区间中使用这些类型(这样我们才能写出 [2..10] 这样简练的表达式)。

Bounded 类型类

Bounded 类型类的实例都有一个上限和下限,分别可通过 maxBoundminBound 函数得到。

> :t minBound
(Bounded a) => a
> :t maxBound
(Bounded a) => a
> minBound :: Int
-21474883648
> maxBound :: Bool
True

注意上面 minBoundmaxBound 的类型声明都是只有一个有类型约束的值,这个值叫做多态常量(polymorphic constant)

如果元组中项的类型都属于 Bounded 类型类的实例,那么这个元组也属于 Bounded 的实例:

> maxBound :: (Bool, Int, Chat)
(True, 2147483647, '\1114111')
Num 类型类

Num 类型类用于表示数值。只有已经属于 ShowEq 的实例类型,才可以成为 Num 类型类的实例。

Floating 类型类

Floating 类型类用于存储浮点数,仅包含 FloatDouble 两种浮点类型。

Integral 类型类

Integral 类型类仅包含整数,实例类型包含 IntInteger(无限版本的 Int)。

参考资料:

  1. Learn you a Haskell for great good!

版权声明: 本文中所有文字版权均属本人所有,如需转载请注明来源。