《Haskell趣学指南》—— 第2章,第2.4节类型类入门

2.4 类型类入门
类型类(typeclass)是定义行为的接口。如果一个类型是某类型类的实例(instance),那它必实现了该类型类所描述的行为。

说得更具体些,类型类是一组函数的集合,如果将某类型实现为某类型类的实例,那就需要为这一类型提供这些函数的相应实现。

可以拿定义相等性的类型类作为例子。许多类型的值都可以通过==运算符来判断相等性,我们先检查一下它的类型签名:

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"
<interactive >:1:0:
   Ambiguous type variable 'a' in the constraint:
     'Read a' arising from a use of 'read' at <interactive>:1:0-7
   Probable fix: add a type signature that fixes these type variable(s)

GHCi跟我们抱怨,搞不清楚我们想要的返回值究竟是什么类型。注意前面我们调用 read之后,都利用所得的结果进行了进一步运算,GHCi也正是通过这一点来辨认类型的。如果我们的表达式的最终结果是一个布尔值,它就知道read的返回类型应该是Bool。在这里它只知道我们要的类型属于Read类型类,但不能明确到底是哪个类型。看一下read函数的类型签名吧:

ghci> :t read
read :: (Read a) => String -> a

注意:
String只是[Char]的一个别名。String与[Char]完全等价、可以互换,不过从现在开始,我们将尽量多用String了,因为String更易于书写,可读性也更高。
可见,read的返回值属于Read类型类的实例,但我们若用不到这个值,它就永远都不会知道返回值的类型。要解决这一问题,我们可以使用类型注解(type annotation)。

类型注解跟在表达式后面,通过::分隔,用来显式地告知Haskell某表达式的类型。

ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> (read "5" :: Float) * 4
20.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')

编译器通常可以辨认出大部分表达式的类型,但也不是万能的。比如,遇到 read "5" 时,编译器就会无法分辨这个类型究竟是Int还是Float了。只有经过运算,Haskell才能明确其类型;同时由于Haskell是一门静态类型语言,它必须在编译之前(或者在GHCi的解释之前)搞清楚所有表达式的类型。所以我们最好提前给它打声招呼:“嘿,这个表达式应该是这个类型,免得你认不出来!”

要Haskell辨认出read的返回类型,我们只需提供最少的信息即可。比如,我们将read的结果放到一个列表中,Haskell即可通过这个列表中的其他元素的类型来分辨出正确的类型。

ghci> [read "True", False, True, False]
[True, False, True, False]

在这里我们将read "True"作为由Bool值组成的列表中的一个元素,Haskell看到了这里的Bool类型,就知道read "True"的类型一定是Bool了。

2.4.5 Enum类型类
Enum的实例类型都是有连续顺序的——它们的值都是可以枚举的。Enum类型类的主要好处在于我们可以在区间中使用这些类型:每个值都有相应的后继(successer)和前趋(predecesor),分别可以通过succ函数和pred函数得到。该类型类包含的类型主要有()、Bool、Char、Ordering、Int、Integer、Float和Double。

ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> [3 .. 5]
[3,4,5]
ghci> succ 'B'
'C'

2.4.6 Bounded类型类
Bounded类型类的实例类型都有一个上限和下限,分别可以通过maxBound和minBound两个函数得到。

ghci> minBound :: Int
-2147483648
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: Bool
True
ghci> minBound :: Bool
False

minBound与maxBound两个函数很有趣,类型都是(Bounded a) => a。可以说,它们都是多态常量(polymorphic constant)。

注意,如果元组中项的类型都属于Bounded类型类的实例,那么这个元组也属于Bounded的实例了。

ghci> maxBound :: (Bool, Int, Char)
(True,2147483647,'\1114111')

2.4.7 Num类型类
Num是一个表示数值的类型类,它的实例类型都具有数的特征。先检查一个数的类型:

ghci> :t 20
20 :: (Num t) => t

看样子所有的数都是多态常量,它可以具有任何Num类型类中的实例类型的特征,如Int、Integer、Float或Double。

ghci> 20 :: Int
20
ghci> 20 :: Integer
20
ghci> 20 :: Float
20.0
ghci> 20 :: Double
20.0

作为例子,我们检查一下*运算符的类型:

ghci> :t (*)
(*) :: (Num a) => a -> a -> a

可见取两个相同类型的数值作为参数,并返回同一类型的数值。由于类型约束,所以(5 :: Int) (6 :: Integer)会导致一个类型错误,而5 * (6 :: Integer)就不会有问题。5既可以是Int类型也可以是Integer类型,但Integer类型与Int类型不能同时用。

只有已经属于Show与Eq的实例类型,才可以成为Num类型类的实例。

2.4.8 Floating类型类
Floating类型类仅包含Float和Double两种浮点类型,用于存储浮点数。

使用Floating类型类的实例类型作为参数类型或者返回类型的函数,一般是需要用到浮点数来进行某种计算的,如sin、cos与sqrt。

2.4.9 Integeral类型类
Integral是另一个表示数值的类型类。Num类型类包含了实数和整数在内的所有的数值相关类型,而Intgeral仅包含整数,其实例类型有Int和Integer。

有一个函数在处理数字时会非常有用,它便是fromIntegral。其类型声明为:

fromIntegral :: (Integral a, Num b) => a -> b

注意:
留意fromIntegral的类型签名中用到了多个类型约束,这是合法的,只要将多个类型约束放到括号里用逗号隔开即可。
从这段类型签名中可以看出,fromIntegeral函数取一个整数作为参数并返回一个更加通用的数值,这在同时处理整数和浮点数时尤为有用。举例来说,length函数的类型声明为:

length :: [a] -> Int

这就意味着,如果取了一个列表的长度,再给它加3.2就会报错(因为这是将Int类型与浮点数类型相加)。面对这种情况,我们即可通过fromIntegral来解决,具体如下:

ghci> fromIntegral (length [1,2,3,4]) + 3.2
7.2

2.4.10 有关类型类的最后总结
由于类型类定义的是一个抽象的接口,一个类型可以作为多个类型类的实例,一个类型类也可以含有多个类型作为实例。比如,Char类型就是多个类型类的实例,其中包括Ord和Eq,我们可以比较两个字符是否相等,也可以按照字母表顺序来比较它们。

有时,一个类型必须在成为某类型类的实例之后,才能成为另一个类型类的实例。比如,某类型若要成为Ord的实例,那它必须首先成为Eq的实例才行。或者说,成为Eq的实例,是成为Ord的实例的先决条件(prerequisite)。这一点不难明白,比如当我们比较两个值的顺序时,一定可以顺便得出这两个值是否相等。

时间: 2025-01-02 05:32:18

《Haskell趣学指南》—— 第2章,第2.4节类型类入门的相关文章

《Haskell趣学指南》—— 第2章,第2.1节显式类型声明

第 2 章 相信类型Haskell趣学指南强大的类型系统是Haskell的秘密武器.在Haskell中,每个表达式都会在编译时得到明确的类型,从而提高代码的安全性.若你写的程序试图让布尔值与数相除,就不会通过编译.这样的好处就是与其让程序在运行时崩溃,不如在编译时捕获可能的错误.Haskell中一切皆有类型,因此编译器在编译时可以得到较多的信息来检查错误. 与Java和Pascal不同,Haskell支持类型推导(type inference).写下一个数,不必额外告诉Haskell说"它是个数

《Haskell趣学指南》—— 第1章,第1.1节调用函数

第 1 章 各就各位,预备!Haskell趣学指南如果你属于那种从不看前言的人,我建议你还是回头看一下本书前言的最后一节比较好.那里讲解了如何使用本书,以及如何通过GHC加载函数. 首先,我们要做的就是进入GHC的交互模式,调用几个函数,以便我们简单地体验一把Haskel.打开终端,输入ghci,可以看到如下欢迎信息: GHCi, version 6.12.3: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ..

《Haskell趣学指南》——导读

目 录 第 1 章 各就各位,预备! 第 1 章第 1 节调用函数第 1 章第 2 节小朋友的第一个函数第 1 章第 3 节列表入门第 1 章第 4 节得州区间2第 1 章第 5 节我是列表推导式第 1 章第 6 节元组 第 2 章 相信类型 第 2 章第 1 节显式类型声明第 2 章第 2 节Haskell的常见类型第 2 章第 3 节类型变量第 2 章第 4 节类型类入门 第 3 章 函数的语法 第 4 章 你好,递归 第 5 章 高阶函数 第 6 章 模块 第 7 章 构造我们自己的类型和

《Haskell趣学指南》—— 第1章,第1.5节我是列表推导式

1.5 我是列表推导式 列表推导式(list comprehension)是一种过滤.转换或者组合列表的方法. 学过数学的你对集合推导式(set comprehension)概念一定不会陌生.通过它,可以从既有的集合中按照规则产生一个新集合.前10个偶数的集合推导式可以写为{2 · x | x∈N, x≤ 10},先不管语法,它的含义十分直观:"取所有小于等于10的自然数,各自乘以2,将所得的结果组成一个新的集合." 若要在Haskell中实现上述表达式,我们可以通过类似take 10

《Haskell趣学指南》—— 第1章,第1.4节得州区间2

1.4 得州区间2该怎样得到一个由1-20所有数组成的列表呢?我们完全可以用手把它们全都录入一遍,但显而易见,这并不是完美人士的方案,完美人士都用区间(range).区间是构造列表的方法之一,而其中的值必须是可枚举的,或者说,是可以排序的. 例如,数字可以枚举为1.2.3.4等.字符同样也可以枚举:字母表就是A-Z所有字符的枚举.然而人名就不可以枚举了,"John"后面是谁?我不知道. 要得到包含1-20中所有自然数的列表,只要录入[1..20]即可,这与录入[1,2,3,4,5,6,

《Haskell趣学指南》—— 第2章,第2.3节类型变量

2.3 类型变量有时让一些函数处理多种类型将更加合理.比如head函数,它可以取一个列表作为参数,返回这一列表头部的元素.在这里列表中元素的类型不管是数值.字符还是列表,都不重要.不管它具体的类型是什么,只要是列表,head函数都能够处理. 猜猜head函数的类型是什么呢?用:t检查一下: ghci> :t head head :: [a] -> a 这里的a是什么?是类型吗?想想我们在前面说过,凡是类型其首字母必大写,所以它不是类型.它其实是个类型变量(type variable),意味着a

《Haskell趣学指南》—— 第1章,第1.3节列表入门

1.3 列表入门在Haskell中,列表是一种单类型的(homogeneous)数据结构,可以用来存储多个类型相同的元素.我们可以在里面装一组数字或者一组字符,但不能把字符和数字装在一起. 列表由方括号括起,其中的元素用逗号分隔开来: ghci> let lostNumbers = [4,8,15,16,23,42] ghci> lostNumbers [4,8,15,16,23,42] 注意:在GHCi中,可以使用let关键字来定义一个常量.在GHCi中执行let a = 1与在脚本中编写a

《Haskell趣学指南》—— 第2章,第2.2节Haskell的常见类型

2.2 Haskell的常见类型接下来我们看几个Haskell中常见的基本类型,比如用于表示数.字符.布尔值的类型. Int意为整数.7可以是Int,但7.2不可以.Int是有界的(bounded),它的值一定界于最小值与最大值之间.注意:我们使用的 GHC 编译器规定 Int 的界限与机器相关.如果你的机器采用64位CPU,那么Int 的最小值一般为−263,最大值为263−1.Integer也是用来表示整数的,但它是无界的.这就意味着可以用它存放非常非常大的数(真的非常非常大!),不过它的效

《Haskell趣学指南》—— 第1章,第1.6节元组

1.6 元组元组(tuple)允许我们将多个异构的值组合成为一个单一的值.从某种意义上讲,元组很像列表.但它们却有着本质的不同.首先就像前面所说,元组是异构的,这表示单个元组可以含有多种类型的元素.其次,元组的长度固定,在将元素存入元组的同时,必须明确元素的数目. 元组由括号括起,其中的项由逗号隔开. ghci> (1, 3) (1,3) ghci> (3, 'a', "hello") (3,'a',"hello") ghci> (50, 50.4