Learn Haskell(四)

强大的类型系统是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)。

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用来提供检测值是否相等的接口。它的两个实现是==和/=。这意 味着如果在一个函数的定义中出现了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
 

5.Tips

Type Class实际上是一个抽象的接口,所以一个类型可以是多种Type Class的实例,同样,一种Type Class有很多实例;

有时候一种类型必须先是一种Type Class的实例才会被允许成为另一个Type Class的实例。

 

 

你可能感兴趣的:(haskell,Type System)