Types and Typeclasses
Last updated
Last updated
之前我们有说过 Haskell 是 Static Type,这表示在编译时期每个表达式的型别都已经确定下来,这提高了代码的安全性。若代码中有让布尔值与数字相除的动作,就不会通过编译。这样的好处就是与其让进程在运行时崩溃,不如在编译时就找出可能的错误。Haskell 中所有东西都有型别,因此在编译的时候编译器可以做到很多事情。
与 Java 和 Pascal 不同,Haskell 支持型别推导。写下一个数字,你就没必要另告诉 Haskell 说"它是个数字",它自己能推导出来。这样我们就不必在每个函数或表达式上都标明其型别了。在前面我们只简单涉及一下 Haskell 的型别方面的知识,但是理解这一型别系统对于 Haskell 的学习是至关重要的。
型别是每个表达式都有的某种标签,它标明了这一表达式所属的范畴。例如,表达式 True
是 boolean
型,"hello"
是个字串,等等。
可以使用 ghci 来检测表达式的型别。使用 :t
命令后跟任何可用的表达式,即可得到该表达式的型别,先试一下:
可以看出,:t
命令处理一个表达式的输出结果为表达式后跟 ::
及其型别,::
读作"它的型别为"。凡是明确的型别,其首字母必为大写。'a'
, 如它的样子,是 Char
型别,易知是个字符 (character)。True
是 Bool
型别,也靠谱。不过这又是啥,检测 "hello"
得一个 [Char]
这方括号表示一个 List,所以我们可以将其读作"一组字符的 List"。而与 List 不同,每个 Tuple 都是独立的型别,于是 (True,'a')
的型别是 (Bool,Char)
,而 ('a','b','c')
的型别为 (Char,Char,Char)
。4==5
一定回传 False
,所以它的型别为 Bool。
同样,函数也有型别。编写函数时,给它一个明确的型别声明是个好习惯,比较短的函数就不用多此一举了。还记得前面那个过滤大写字母的 List Comprehension 吗?给它加上型别声明便是这个样子:
removeNonUppercase
的型别为 [Char]->[Char]
,从它的参数和回传值的型别上可以看出,它将一个字串映射为另一个字串。[Char]
与 String
是等价的,但使用 String
会更清晰:removeNonUppercase :: String -> String
。编译器会自动检测出它的型别,我们还是标明了它的型别声明。要是多个参数的函数该怎样?如下便是一个将三个整数相加的简单函数。
参数之间由 ->
分隔,而与回传值之间并无特殊差异。回传值是最后一项,参数就是前三项。稍后,我们将讲解为何只用 ->
而不是 Int,Int,Int->Int
之类"更好看"的方式来分隔参数。
如果你打算给你编写的函数加上个型别声明却拿不准它的型别是啥,只要先不写型别声明,把函数体写出来,再使用 :t
命令测一下即可。函数也是表达式,所以 :t
对函数也是同样可用的。
如下是几个常见的型别:
Int 表示整数。7 可以是 Int,但 7.2 不可以。Int 是有界的,也就是说它由上限和下限。对 32 位的机器而言,上限一般是 2147483647
,下限是 -2147483648
。
Integer 表示...呃...也是整数,但它是无界的。这就意味着可以用它存放非常非常大的数,我是说非常大。它的效率不如 Int 高。
Float 表示单精度的浮点数。
Double 表示双精度的浮点数。
Bool 表示布尔值,它只有两种值:True
和 False
。
Char 表示一个字符。一个字符由单引号括起,一组字符的 List 即为字串。
Tuple 的型别取决于它的长度及其中项的型别。注意,空 Tuple 同样也是个型别,它只有一种值:()
。
你觉得 head
函数的型别是啥?它可以取任意型别的 List 的首项,是怎么做到的呢?我们查一下!
嗯! a
是啥?型别吗?想想我们在前面说过,凡是型别其首字母必大写,所以它不会是个型别。它是个型别变量,意味着 a 可以是任意的型别。这一点与其他语言中的泛型 (generic) 很相似,但在 Haskell 中要更为强大。它可以让我们轻而易举地写出型别无关的函数。使用到型别变量的函数被称作"多态函数 ",head
函数的型别声明里标明了它可以取任意型别的 List 并回传其中的第一个元素。
在命名上,型别变量使用多个字符是合法的,不过约定俗成,通常都是使用单个字符,如 a
, b
,c
,d
...
还记得 fst
?我们查一下它的型别:
可以看到fst
取一个包含两个型别的 Tuple 作参数,并以第一个项的型别作为回传值。这便是 fst
可以处理一个含有两种型别项的 pair 的原因。注意,a
和 b
是不同的型别变量,但它们不一定非得是不同的型别,它只是标明了首项的型别与回传值的型别相同。
型别定义行为的接口,如果一个型别属于某 Typeclass,那它必实现了该 Typeclass 所描述的行为。很多从 OOP 走过来的人们往往会把 Typeclass 当成面向对象语言中的 class
而感到疑惑,呃,它们不是一回事。易于理解起见,你可以把它看做是 Java 的 interface。
==
函数的型别声明是怎样的?
有意思。在这里我们见到个新东西:=>
符号。它左边的部分叫做型别约束。我们可以这样阅读这段型别声明:"相等函数取两个相同型别的值作为参数并回传一个布尔值,而这两个参数的型别同在 Eq 类之中(即型别约束)"
Eq 这一 Typeclass 提供了判断相等性的接口,凡是可比较相等性的型别必属于 Eq
class。
elem
函数的型别为: (Eq a)=>a->[a]->Bool
。这是它在检测值是否存在于一个 List 时使用到了==的缘故。
几个基本的 Typeclass:
Eq 包含可判断相等性的型别。提供实现的函数是 ==
和 /=
。所以,只要一个函数有Eq类的型别限制,那么它就必定在定义中用到了 ==
和 /=
。刚才说了,除函数以外的所有型别都属于 Eq
,所以它们都可以判断相等性。
Ord 包含可比较大小的型别。除了函数以外,我们目前所谈到的所有型别都属于 Ord
类。Ord
包中包含了<, >, <=, >=
之类用于比较大小的函数。compare
函数取两个 Ord
类中的相同型别的值作参数,回传比较的结果。这个结果是如下三种型别之一:GT, LT, EQ
。
型别若要成为Ord的成员,必先加入Eq家族。
Show 的成员为可用字串表示的型别。目前为止,除函数以外的所有型别都是 Show
的成员。操作 Show Typeclass,最常用的函数表示 show
。它可以取任一Show的成员型别并将其转为字串。
Read 是与 Show
相反的 Typeclass。read
函数可以将一个字串转为 Read
的某成员型别。
一切良好,如上的所有型别都属于这一 Typeclass。尝试 read "4"
又会怎样?
ghci 跟我们说它搞不清楚我们想要的是什么样的回传值。注意调用 read
后跟的那部分,ghci 通过它来辨认其型别。若要一个 boolean
值,他就知道必须得回传一个 Bool
型别的值。但在这里它只知道我们要的型别属于 Read Typeclass,而不能明确到底是哪个。看一下 read
函数的型别声明吧:
看,它的回传值属于 ReadTypeclass,但我们若用不到这个值,它就永远都不会得知该表达式的型别。所以我们需要在一个表达式后跟::
的型别注释,以明确其型别。如下:
编译器可以辨认出大部分表达式的型别,但遇到 read "5"
的时候它就搞不清楚究竟该是 Int 还是 Float 了。只有经过运算,Haskell 才会明确其型别;同时由于 Haskell 是静态的,它还必须得在 编译前搞清楚所有值的型别。所以我们就最好提前给它打声招呼:"嘿,这个表达式应该是这个型别,省得你认不出来!"
Enum 的成员都是连续的型别 -- 也就是可枚举。Enum
类存在的主要好处就在于我们可以在 Range
中用到它的成员型别:每个值都有后继子 (successer) 和前置子 (predecesor),分别可以通过 succ
函数和 pred
函数得到。该 Typeclass 包含的型别有:()
, Bool
, Char
, Ordering
, Int
, Integer
, Float
和 Double
。
Bounded 的成员都有一个上限和下限。
minBound
和 maxBound
函数很有趣,它们的型别都是 (Bounded a) => a
。可以说,它们都是多态常量。
如果其中的项都属于 Bounded
Typeclass,那么该 Tuple 也属于 Bounded
Num 是表示数字的 Typeclass,它的成员型别都具有数字的特征。检查一个数字的型别:
看样子所有的数字都是多态常量,它可以作为所有 Num
Typeclass中的成员型别。以上便是 Num
Typeclass 中包含的所有型别,检测 *
运算子的型别,可以发现它可以处理一切的数字:
它只取两个相同型别的参数。所以 (5 :: Int) * (6 :: Integer)
会引发一个型别错误,而 5 * (6 :: Integer)
就不会有问题。
型别只有亲近 Show
和 Eq
,才可以加入 Num
。
Integral 同样是表示数字的 Typeclass。Num
包含所有的数字:实数和整数。而 Integral 仅包含整数,其中的成员型别有 Int
和 Integer
。
Floating 仅包含浮点型别:Float
和 Double
。
有个函数在处理数字时会非常有用,它便是 fromIntegral。其型别声明为: fromIntegral :: (Num b, Integral a) => a -> b
。从中可以看出,它取一个整数做参数并回传一个更加通用的数字,这在同时处理整数和浮点时会尤为有用。举例来说,length
函数的型别声明为:length :: [a] -> Int
,而非更通用的形式,如 length :: (Num b) => [a] -> b
。这应该是历史原因吧,反正我觉得挺蠢。如果取了一个 List 长度的值再给它加 3.2 就会报错,因为这是将浮点数和整数相加。面对这种情况,我们就用 fromIntegral (length [1,2,3,4]) + 3.2
来解决。
注意到,fromIntegral
的型别声明中用到了多个型别约束。如你所见,只要将多个型别约束放到括号里用逗号隔开即可。