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)。
1.显式类型声明
除了表达式之外,函数也是有类型的。我们在定义函数的时候,可以显式给函数声明其类型。我们在前面讲过一个去处字符串中大写字母的List Comprehension:
removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]
对于这样一个函数,很明显,其输入和输出都是字符串,也就是字符List,因此,我们可以这样声明函数的类型:
removeNonUppercase :: [Char]->[Char]
上面这个声明的含义是,函数removeNonUppercase接 收一个[Char]参数,并且返回一个[Char]。那怎么去指定一个接收多个参数的函数的类型呢?比如说有一个函数叫addThree,接收三个参数, 并且将这三个参数的值相加并且返回。我们可以这样指定addThree的函数类型:
addThree :: Int->Int->Int->Int
也就是说,最后一个会被当做返回值来解析,前面的都会被当做参数来解析。如果说你不知道你要写的函数到底应该是什么类型,你可以先把函数写出来,然后使用:t命令看看到底是什么类型,最后再补上函数类型指定。
2.常见的Haskell类型
类型
说明
Int
整型,但是能表示的整数有界限(达到一定程度就会溢出),效率更高
Integer
整型,能够表示的整数没有界限,效率低
Float
单精度浮点数
Double
双精度浮点数
Bool
布尔值,只有True和False两个值
Char
单个Unicode字符
Tuple
具体的Tuple类型取决于元素的类型和个数,理论上有无数Tuple类型,但是实际上Tuple最多只能有63个元素
3.类型变量(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,返回这种类型的一个值。
我们再看看fst函数的类型定义:
Prelude> :t fst fst :: (a, b) –> a
这个函数接收一个pair,然后返回第一个元素,至于这个pair的元素可以是任何类型,这里的a,b都是类型变量。需要说明的是,这里的a和b虽然都是类型变量,但是不意味着他们一定是不同的类型。
4.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 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是一种为那些可以将值放在某种顺序排列中的类型设计的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这个Type Class的实例的值可以被显示为字符串。对于所有属于Show这个Type Class的实例的类型来说,使用最多的函数式show(s小写)。我们看几个例子:
Prelude> show 3 "3" Prelude> show True "True"
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 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实例的类型有一个上限值和一个下限值。分别可以使用minBound和maxBound查看:
Prelude> minBound::Int -2147483648 Prelude> maxBound::Int 2147483647
minBound和maxBound的类型都是Bounded a=>a。准确来说,他们是多态常量。Tuple中所有元素类型都是Bounded的话,那么这个Tuple也被认为是Bounded的实例。
Num是数字Type Class,它的实例都是数字。所有的数字都是多态常量。也就是说我们可以将它制定成Num下属类型中的任何一种:
Prelude> 6::Int 6 Prelude> 6::Float 6.0
要成为Num Type Class的实例,这个类型必须要已经是Eq和Show Type Class的实例。
顾名思义,这种Type Class的实例类型就是用来存储浮点数的,就两种类型Float和Double。
包括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
5.Tips
Type Class实际上是一个抽象的接口,所以一个类型可以是多种Type Class的实例,同样,一种Type Class有很多实例;
有时候一种类型必须先是一种Type Class的实例才会被允许成为另一个Type Class的实例。