HasKell类型
第 2 章 相信类型 强大的类型系统是Haskell的秘密武器。
在Haskell中,每个表达式都会在编译时得到明确的类型,从而提高代码的安全性。若你写的程序试图让布尔值与数相除,就不会通过编译。这样的好处就是与其让程序在运行时崩溃,不如在编译时捕获可能的错误。Haskell中一切皆有类型,因此编译器在编译时可以得到较多的信息来检查错误。
tu2-1.jpg
与Java和Pascal不同,Haskell支持类型推导 (type inference)。写下一个数,不必额外告诉Haskell说“它是个数”,Haskell自己就能推导出来。
在此之前,我们对Haskell类型的讲解还只是一扫而过而已,到这里不妨打起精神来,因为理解这套类型系统对于Haskell的学习是至关重要的。
2.1 显式类型声明 我们可以使用GHCi来检查表达式的类型。通过:t 命令,后跟任何合法的表达式,即可得到该表达式的类型。先试一下:
ghci> :t ‘a’ ‘a’ :: Char ghci> :t True True :: Bool ghci> :t “HELLO!” “HELLO!” :: [Char] ghci> :t (True, ‘a’) (True, ‘a’) :: (Bool, Char) ghci> :t 4 == 5 4 == 5 :: Bool
:: 读作“它的类型为”。凡是明确的类型,其首字母必为大写。’a’ 是Char 类型,意为字符(character)类型;True 是Bool 类型,意为布尔(Boolean)类型;而字符串”HELLO!” 的类型显示为[Char] ,其中的方括号表示这是个列表,所以我们可以将它读作“一组字符的列表”。元组与列表不同,每个不同长度的元组都有其独立的类型,于是(True, ‘a’) 的类型为(Bool, Char) ,而(‘a’, ‘b’, ‘c’) 的类型为(Char, Char, Char) 。4 == 5 必返回False ,因此它的类型为Bool 。
tu2-2.jpg
同样,函数也有类型。编写函数时,给它一个显式的类型声明是一个好习惯(至于比较短的函数就不必多此一举了)。从此刻开始,我们会对我们创建的所有函数都给出类型声明。
还记得第1章中我们写的那个过滤大写字母的列表推导式吗?给它加上类型声明便是这个样子:
removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [ c | c <- st, c elem
[‘A’..’Z’]]
这里removeNonUppercase 的类型为[Char] -> [Char] ,意为它取一个字符串作为参数,返回另一个字符串作为结果。
不过,多个参数的函数该怎样指定其类型呢?下面便是一个将三个整数相加的简单函数。
addThree :: Int -> Int -> Int -> Int addThree x y z = x + y + z
参数与返回类型之间都是通过-> 分隔,最后一项就是返回值的类型了。(在第5章,我们将讲解为何统统采用-> 分隔,而非Int, Int, Int -> Int 之类“更好看”的方式。)
如果你打算给自己的函数加上类型声明,却拿不准它的类型是什么。那就先不管它,把函数先写出来,再使用:t命令测一下即可。因为函数也是表达式,所以:t对函数也是同样可用的。
2.2 Haskell的常见类型 接下来我们看几个Haskell中常见的基本类型,比如用于表示数、字符、布尔值的类型。
Int 意为整数。7 可以是Int ,但7.2 不可以。Int 是有界的(bounded),它的值一定界于最小值与最大值之间。 注意:
我们使用的 GHC 编译器规定 Int 的界限与机器相关。如果你的机器采用64位CPU,那么Int 的最小值一般为−263 ,最大值为263 −1。
Integer 也是用来表示整数的,但它是无界的。这就意味着可以用它存放非常非常大的数(真的非常非常大!),不过它的效率不如Int 高。拿下面的函数作为例子,可以将下面的函数保存到一个文件中: factorial :: Integer -> Integer factorial n = product [1..n]
然后通过:l 将它装载入GHCi并进行测试:
ghci> factorial 50 30414093201713378043612608166064768844377641568960512000000000000
Float 表示单精度浮点数。将下面的函数加入刚才的文件: circumference :: Float -> Float circumference r = 2 * pi * r
随后装载并测试:
ghci> circumference 4.0 25.132742
Double 表示双精度浮点数。双精度的数值类型中的位是一般的数值类型的两倍,这些多出来的位使它的精度更高,同时也占据更大的内存空间。继续将下面的这个函数加入文件: circumference’ :: Double -> Double circumference’ r = 2 * pi * r
装载并测试。可以特别留意circumference 与circumference’ 两者在精度上的差异。
ghci> circumference’ 4.0 25.132741228718345
Bool 表示布尔值,它只有两种值,即True 和False 。 Char 表示一个Unicode字符。一个字符由单引号括起,一组字符的列表即字符串。 元组也是类型,不过它们的类型取决于其中项的类型及数目,因而理论上可以有无限种元组类型(实际上元组中的项的数目最大为62——已经远大于我们日常的需求了)。注意,空元组同样也是个类型,它只有一种值,即()。 2.3 类型变量 有时让一些函数处理多种类型将更加合理。比如head 函数,它可以取一个列表作为参数,返回这一列表头部的元素。在这里列表中元素的类型不管是数值、字符还是列表,都不重要。不管它具体的类型是什么,只要是列表,head函数都能够处理。
猜猜head 函数的类型是什么呢?用:t 检查一下:
ghci> :t head head :: [a] -> a
这里的a 是什么?是类型吗?想想我们在前面说过,凡是类型其首字母必大写,所以它不是类型。它其实是个类型变量 (type variable),意味着a 可以是任何类型。
通过类型变量,我们可以在类型安全 (type safe))的前提下,轻而易举地编写能够处理多种类型的函数。这一点与其他语言中的泛型(generic)很相似,但在Haskell中要更为强大,更容易写出通用的函数。
tu2-3.jpg
使用了类型变量的函数被称作多态函数 (polymorphic function)。head函数即为此例,从它的类型声明中可以看出,它的参数类型为任意类型的元素组成的列表,返回的类型也正是该类型。
注意:
在命名上,类型变量使用多个字符是合法的,不过约定俗成,通常都是使用单个字符作为名字,如a,b,c,d…
还记得fst 吗?它可以返回一个序对中的首项。查一下它的类型:
ghci> :t fst fst :: (a, b) -> a
可以看出fst 取一个元组作为参数,且返回类型与元组中首项的类型相同。这便是fst 能够处理任何类型序对的原因。注意,a 和b 是不同的类型变量,并非特指二者表示的类型不同,这就意味着,在这段类型声明中元组首项的类型与返回值的类型可以相同。
2.4 类型类入门 类型类 (typeclass)是定义行为的接口。如果一个类型是某类型类的实例 (instance),那它必实现了该类型类所描述的行为。
说得更具体些,类型类是一组函数的集合,如果将某类型实现为某类型类的实例,那就需要为这一类型提供这些函数的相应实现。
tu2-4.jpg
可以拿定义相等性的类型类作为例子。许多类型的值都可以通过==运算符来判断相等性,我们先检查一下它的类型签名:
ghci> :t (==) (==) :: (Eq a) => a -> a -> Bool
注意,判断相等性的==运算符实际上是一个函数,+、-、*、/ 之类的运算符也是同样。如果一个函数的名字皆为特殊字符,则默认为中缀函数。若要检查它的类型、传递给其他函数调用或者作为前缀函数调用,就必须得像上面的例子那样,用括号将它括起来。
在这里我们见到了一个新东西,即=> 符号。它的左侧叫做类型约束 (type constraint)。我们可以这样读这段类型声明:“相等性函数取两个相同类型的值作为参数并返回一个布尔值,而这两个参数的类型同为Eq 类的实例。”
Eq 这一类型类提供了判断相等性的接口,凡是可比较相等性的类型必属于Eq 类。Haskell中所有的标准类型都是Eq 类的实例(除与输入输出相关的类型和函数之外)。
注意:
千万不要将Haskell的类型类与面向对象语言中类(Class)的概念混淆。
接下来我们将观察几个Haskell中最常见的类型类,比如判断相等性的类型类、判断次序的类型类、打印为字符串的类型类等。
2.4.1 Eq 类型类 前面已提到,Eq 类型类用于可判断相等性的类型,要求它的实例必须实现== 和/= 两个函数。如果函数中的某个类型变量声明了属于Eq的类型约束,那么它就必然定义了== 和/= 。也就是说,对于这一类型提供了特定的函数实现。下面即是操作Eq 类型类的几个实例的例子:
ghci> 5 == 5 True ghci> 5 /= 5 False ghci> ‘a’ == ‘a’ True ghci> “Ho Ho” == “Ho Ho” True ghci> 3.432 == 3.432 True
2.4.2 Ord 类型类 Ord 类型类用于可比较大小的类型。作为一个例子,我们先看看大于号也就是> 运算符的类型声明:
ghci> :t (>) (>) :: (Ord a) => a -> a -> Bool
运算符的类型与==很相似。取两个参数,返回一个Bool 类型的值,告诉我们这两个参数是否满足大于关系。
除了函数以外,我们目前所谈到的所有类型都是Ord 的实例。Ord 类型类中包含了所有标准的比较函数,如<、>、<=、>= 等。
compare 函数取两个Ord 中的相同类型的值作为参数,返回一个Ordering 类型的值。Ordering 类型有GT 、LT 和 EQ 三种值,分别表示大于、小于和等于。
ghci> “Abrakadabra” < “Zebra”
True
ghci> “Abrakadabra” compare
“Zebra”
LT
ghci> 5 >= 2
True
ghci> 5 compare
3
GT
ghci> ‘b’ > ‘a’
True
2.4.3 Show 类型类 Show 类型类的实例为可以表示为字符串的类型。目前为止,我们提到的除函数以外的所有类型都是Show 的实例。操作Show 类型类的实例的函数中,最常用的是show 。它可以取任一Show 的实例类型作为参数,并将其转为字符串:
ghci> show 3 “3” ghci> show 5.334 “5.334” ghci> show True “True”
2.4.4 Read 类型类 Read 类型类可以看做是与Show 相反的类型类。同样,我们提到的所有类型都是Read 的实例。read 函数可以取一个字符串作为参数并转为Read 的某个实例的类型。
ghci> read “True” || False True ghci> read “8.2” + 3.8 12.0 ghci> read “5” - 2 3 ghci> read “[1,2,3,4]” ++ [3] [1,2,3,4,3]
至此一切良好。但是,尝试read “4” 又会怎样?
ghci> read “4”