类型系统
强大的类型系统是 Haskell的 一个非常大的优势。
Haskell 所有表达式类型在编译时判断。这样的话,可以使得代码更加安全,比如说,拿一个整数和一个字符串进行除法运算是没办法进行的,那么在编译器就会直接报错,不会等到运行时程序崩溃才知道。Haskell 与 Java 不一样,Haskell 能够进行类型推断
(Type Inference),也就是说,你不需要明确的说 100 是个数字,或者说是整型,编译时能推断出这是一个整型。
在 GHCi 中,我们可以使用 :t
命令来检测一个表达式的类型。
Prelude> :t 'q'
'q' :: Char
Prelude> :t "aaa"
"aaa" :: [Char]
::
操作符的含义是「具有 … 类型」。也就是说,根据上面的结果,我们知道,字符
q 的类型是 「Char」。一般来说,Haskell 的类型的首字母都是大写,比如上面提到的 Char,还有 Bool 或者 Boolean。[]
代表 List,[Char]
代表元素类型为 Char 的 List。()
则代表 Tuple,('a','a')
的类型是 (Char,Char)
。
显式类型声明
除了表达式之外,函数也是有类型的。我们在定义函数的时候,可以显式地给函数声明其类型。我们在前面讲过一个去除字符串中大写字母的 List Comprehension:
removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]
对于这样一个函数,很明显,其输入和输出都是字符串,也就是字符的 List,因此,我们可以这样声明函数的类型:
removeNonUppercase :: [Char]->[Char]a
上面这个声明的含义是,函数 removeNonUppercase
接收一个[Char]
类型的参数(例如字符串),并且返回一个 [Char]
(例如字符串)。那怎么去指定一个接收多个参数的函数的类型呢?比如说有一个函数叫 addThree
,接收三个参数,将这三个参数的值相加并且返回。我们可以这样指定 addThree 的函数类型:
addThree :: Int->Int->Int->Int
也就是说,最后一个会被当做返回值来解析,前面的都会被当做参数来解析。如果说你不知道你要写的函数到底应该是什么类型,你可以先把函数写出来,然后使用 :t
命令看看到底是什么类型,最后再补上函数类型。
常见的Haskell类型
类型 | 说明 |
---|---|
Int | 整型,但是能表示的整数有界限(达到一定程度就会溢出),效率更高 |
Integer | 整型,能够表示的整数没有界限,效率低 |
Float | 单精度浮点数 |
Double | 双精度浮点数 |
Bool | 布尔值,只有 True 和 False 两个值 |
Char | 单个Unicode字符 |
Tuple | 具体的 Tuple 类型取决于元素的类型和个数,理论上有无数 Tuple 类型,但是实际上Tuple最多只能有63个元素 |
类型变量(Type Variable)
有时候函数需要能够处理多种类型的数据,我们以 head
函数为例。首先看看 head 函数的类型:
Prelude> :t head
head :: [a] –> a
我们可以看到,函数 head
接收一个 List 作为输入,返回 List 中的一个元素。但是这个元素到底是 Char 还是 Int 还是 Bool 并不重要。这个 a 是什么?我们说过所有的类型都是以大写字母打头的,a 显然不是一种我们所不知道的类型。a 实际上就是我们这里说的类型变量的一个例子。类型变量能够允许函数以一种安全的方式操作多种类型,这一点类似于 Java 中的泛型。使用类型变量的函数在 Haskell 中称为多态函数(Polymorphyc function)。head 函数的定义的含义是:head 接收一个装有任何元素的 List,返回这种类型的一个值。英语中单词 a
也表示泛指, a pen, a apple 等等。
我们再看看 fst 函数的类型定义:
Prelude> :t fst
fst :: (a, b) –> a
这个函数接收一个 pair
,然后返回第一个元素,至于这个 pair 的元素可以是任何类型,这里的a,b都是类型变量。需要说明的是,这里的 a 和 b 虽然都是类型变量,但是不意味着他们一定是不同的类型。a,b 这种类型变量就像占位符变量一样,表示这个地方有一个某某类型的变量。
Type Class
Type Class 我也不知道该怎么翻译比较合适。Type Class 实际上是一种接口,它定义一些行为,当某个变量是这个 Type Class 的实例时,那么它可以实现这个 Type Class 所描述的行为。Type Class 一般指定一组函数,一个变量是该 Type Class 的实例,我们就需要确定这些函数对于这个变量本身有什么意义(也就是说这个变量要有自己的实现)。
定义相等性的 Type Class 就是一个很好的例子。很多类型都可以用 ==
来看值是否相等。我们先看看 ==
运算符的函数签名:
Prelude> :t (==)
(==) :: Eq a => a -> a –> Bool
实际上 ==
是一个函数,基本上 +
,-
,*
以及几乎所有的运算符都是函数。这里出现了一个新的符号 =>
,所有出现在这个符号之前的部分叫做 class constraint(类的约束)。这个函数类型的意思是:==
函数接收两个值,他们同样属于类型 Eq
,函数最终返回一个 Bool 值。
Eq
就属于 Type Class,它提供了判断值是否相等的接口。而这些值必须是相同类型才有比较的意义,这些值可以是 Eq
的实例。事实上,在标准的 Haskell 中,几乎所有类型都是 Eq
的实例。需要特别指出的是,Type Class 并不是面向对象编程语言中的 Class。下面我们一起看看 Haskell 中常见的几种 Type Class:
- Eq
Eq
用来提供检测值是否相等的接口。它的两个实现是 ==
和 /=
。这意味着如果在一个函数的定义中出现了 Eq class constraint,那么这个函数的定义中肯定用到了 ==
或者是 /=
。如果一种类型实现一个函数,他就要定义使用这个类型的值时,该函数到底做些什么。我们看几个 Eq 实例进行相等性比较时的例子:
Prelude> 5 == 5
True
Prelude> 'q' == 'q'
True
Prelude> "Hello"=="hello"
False
Prelude> "Hello"=="Hello"
True
Prelude> pi == 3.14
False
我们可以看到,字符串的比较规则是遵循 List 的相等性比较,与 Java 中的比较引用是不一样的。
- Ord
Ord
是一种为那些可以将值放在某种顺序排列中的类型设计的 Type Class。我们看看 >
函数的类型:
Prelude> :t (>)
(>) :: Ord a => a -> a –> Bool
>
与 ==
比较类似,都接收两个参数,然后返回一个 Bool 值。Ord Type Class 涉及到了所有的比较函数:>
、<
、 >=
、 <=
。
compare
函数接收两个参数,这两个参数的类型都是 Ord
的实例,然后返回一个 Ordering
。Ordering 是一个值可以是 GT、LT 或者 EQ 的类型,分别代表大于、小于和等于。我们看几个例子:
Prelude> "abcd" `compare` "bbcd"
LT
Prelude> "abcd" `compare` "abbd"
GT
Prelude> "abcd" `compare` "abcd"
EQ
- Show
类型是 Show
这个 Type Class 的实例的值可以被显示为字符串。对于所有属于 Show 这个 Type Class 的实例的类型来说,使用最多的函数式 show(s小写)。我们看几个例子:
Prelude> show 3
"3"
Prelude> show True
"True"
- Read
Read 可以看做是 Show 的反面。read 函数接收一个字符串,然后返回一个类型是 Read 的实例的值。看例子:
Prelude> read "True" || False
True
Prelude> read "5"-2
3
Prelude> read "[1,2,3,4]" ++ [5]
[1,2,3,4,5]
目前为止都一切正常,我们再看一个例子:
Prelude> read "5"
<interactive>:30:1:
Ambiguous type variable `a0' in the constraint:
(Read a0) arising from a use of `read'
Probable fix: add a type signature that fixes these type variable(s)
In the expression: read "5"
In an equation for `it': it = read "5"
当我们直接 read "5"
时,GHCi 不知道该返回什么。我们之前的例子都将 read 返回的结果再参与某种运算,这样 GHCi 才好进行类型推断,这就是为什么 read "5"
没办法返回值的原因。我们看一下read 函数的类型:
Prelude> :t read
read :: Read a => String –> a
我们看到,read 函数接收 String,但是返回一个类型是 Read 的实例的值。但是类型是 Read 实例的类型太多了,GHCi 不知道到底选哪一种类型。这种情况下,我们可以使用类型注解(type annotation)。我们看例子是最直接的:
Prelude> read "5" :: Int
5
Prelude> read "5" :: Float
5.0
对于 read 来说还需要举一个例子:
Prelude> [read "True",False,True,False]
[True,False,True,False]
因为 List 中的每一个元素必须属于同种类型,所以 read "True"
的返回值必须和其他元素类型一样,也就是 Bool,这样,GHCi 就知道该怎么返回值了。
- Enum
Enum 的实例是那种值有序的类型——他们的值可以被枚举。Enum Type Class 最大的优势是可以在 Ranges 中使用其值。他们还定义了successors 和 predecessors, 我们可以分别通过 succ 和 pred 两个函数获得。Bool、Char、Ordering、Int、Integer、Float、Double 是这个 Type Class 的实例,我们看例子:
Prelude> ['a'..'e']
"abcde"
Prelude> [LT .. GT]
[LT,EQ,GT]
Prelude> [3 .. 5]
[3,4,5]
Prelude> succ 'B'
'C'
Prelude> pred 'B'
'A'
- Bounded
那些是 Bounded 实例的类型有一个上限值和一个下限值。分别可以使用 minBound 和 maxBound 查看:
Prelude> minBound::Int
-2147483648
Prelude> maxBound::Int
2147483647
minBound 和 maxBound 的类型都是 Bounded a=>a
。准确来说,他们是多态常量。Tuple 中所有元素类型都是 Bounded 的话,那么这个 Tuple 也被认为是 Bounded 的实例。
- Num
Num 是数字 Type Class,它的实例都是数字。所有的数字都是多态常量。也就是说我们可以将它制定成 Num 下属类型中的任何一种:
Prelude> 6::Int
6
Prelude> 6::Float
6.0
要成为 Num Type Class 的实例,这个类型必须要已经是 Eq 和 Show Type Class 的实例。
- Floating
顾名思义,这种 Type Class 的实例类型就是用来存储浮点数的,就两种类型 Float 和 Double。
- Integral
包括 Int 和 Integer 两种。介绍两个函数 fromIntegral 和 length,先看看两个函数的签名,再看看怎么使用:
Prelude> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b
Prelude> :t length
length :: [a] –> Int
Prelude> fromIntegral (length [1,2,3,4]) + 3.4
7.4
Tips
Type Class 实际上是一个抽象的接口,所以一个类型可以是多种 Type Class 的实例,同样,一种 Type Class 有很多实例;
有时候一种类型必须先是一种 Type Class 的实例才会被允许成为另一个Type Class 的实例。