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

    xiaoxiao2024-08-20  92

    本节书摘来自异步社区《Haskell趣学指南》一书中的第2章,第2.4节类型类入门,作者 【斯洛文尼亚】Miran Lipovaca,更多章节内容可以访问云栖社区“异步社区”公众号查看

    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.27.2``

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

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

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)