类型类

数据类型与函数 一章中,我们阐述了多态性分为 参数多态性(Parametric PolyMorphism)临时多态性(Ad-hoc PolyMorphism) , 并着重讨论了参数多态性。本章我们讲解的类型类,是一种允许使用临时多态性的语法。

临时多态性允许某个变量为若干类型中的任意一种,这是因为它对于每种类型都进行了单独的定义[1]

提示:与参数多态性明显不同的是,参数多态性中类型的变量指任意类型,而临时多态性的类型并不是任意的。

类型类的声明与实例的实现

仍然沿用Figure''类型,并定义一个SolidFigure类型表示立体图形。

-- code4.hs
{-# LANGUAGE DuplicateRecordFields #-}
-- 开启扩展 DuplicateRecordFields 允许记录语法中的域名称重复
type Height = Double

data SolidFigure = 
    Sphere {getDiameter :: Diameter}
    | Cuboid {getLength :: Length, getWidth :: Width, getHeight :: Height}
    | Cylinder {getDiameter :: Diameter, getHeight :: Height}
    deriving (Show)

SolidFigure类型定义了三个立体图形,Sphere表示球体,Cuboid为长方体,Cylinder为圆柱体,当然还有其他立体图形,读者可以根据自己的喜好自行添加。

在前面章节中我们定义过的judgeShape函数,它可以用于判断Figure''类型的形状,我们希望定义一个更通用的函数,它既可以判断平面图形Figure''也可以判断立体图形SolidFigure。类型类恰好就可以胜任这种情形。

我们定义一个Judgeable类型类用来表示图形是可判断的,如下:

-- code4.hs
class Judgeable a where 
    judgeType :: a -> String
    tellShape :: a -> String

类型类以class关键字开头,并定义了两个函数judgeTypetellShape用于判断图形的类型和图形的形状。

此时,我们已经构建了具有临时多态性的函数,接下来只需要分别对两种待处理的类型进行实例化。

-- code4.hs
-- {-# LANGUAGE InstanceSigs #-}

instance Judgeable Figure'' where  
    -- judgeType :: Figure'' -> String
    judgeType _ = "A Figure"
    -- tellShape :: Figure'' -> String
    tellShape (Circle'' _) = "A Circle"
    tellShape (Rectangle'' {}) = "A Rectangle"
    tellShape (Triangle'' {}) = "A Triangle"

instance Judgeable SolidFigure where 
    -- judgeType :: SolidFigure -> String
    judgeType _ = "A SolidFigure"
    -- tellShape :: SolidFigure -> String
    tellShape (Sphere _) = "A Sphere"
    tellShape (Cuboid {}) = "A Cuboid"
    tellShape (Cylinder {}) = "A Cylinder"

使用关键字instance将类型类Judgeable中参数a实例化为对应类型,并为实例定义相应的函数。

注意:在Haskell 2010标准中,类型类实例中默认不允许使用类型签名,可以使用扩展 {-# LANGUAGE InstanceSigs #-}允许这一点,这在后续的标准中已经被移除

下面尝试在GHCi中使用这个类型类:

Prelude> :load code4.hs
[1 of 1] Compiling Main             ( code4.hs, interpreted )
Ok, one module loaded.
Prelude> tellShapeInfo x = "This is " ++ tellShape x ++ " which is " ++ judgeType x
Prelude> tellShapeInfo (Sphere 1)
"This is A Sphere which is A SolidFigure"
Prelude> tellShapeInfo (Circle'' 1)
"This is A Circle which is A Figure"

类型类约束

一般地,我们可以对类型参数施加类型类约束(也称为上下文),例如上一节中的tellShapeInfo函数实际上为:

-- code4.hs

tellShapeInfo :: Judgeable a => a -> [Char]
tellShapeInfo x = "This is " ++ tellShape x ++ " which is " ++ judgeType x 

即接受的参数的类型必须是已经实现为Judgeable类型类实例的类型。

或者也可以在定义类型类时添加其他类型类的约束:

-- code4.hs

class Judgeable a => Dimension a where 
  dim :: a -> Int

instance Dimension Figure'' where
  dim = const 2

instance Dimension SolidFigure where
  dim = const 3

这里定义了一个Dimension类型类用于给出图形的维数,该类型类中类型参数a受到Judgeable的约束,任何实现Dimension实例的类型必须首先是可判定的。

在定义类型上也可以使用类型类进行约束,根据约束位于等号左侧还是右侧可以分为两类,前者需要使用{-# LANGUAGE DatatypeContexts #-}扩展,但被广泛认为是一个错误,可由其他方案代替(读者可查看相关解答);后者则属于存在类型,见下文

常用类型类

Haskell中内置了众多的类型类,由于这些类型类可以自动派生出实例,本小结仅对这些类型类进行简要的介绍。

相等类型类 Eq

Eq类型类提供了判断相等(不等)的函数,其定义如下:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
  {-# MINIMAL (==) | (/=) #-} 

Eq类型类中(==)函数用于判断相等,而(/=)用于判断不等。根据尾行的提示,在对类型类进行实例化时,只需要实现(==)(/=)中的一个即可(最小实现)。

有序类型类 Ord

Ord类型类用于实现排序相关的函数,一个能够排序的类型必然是能够判断相等的(或者说受到相等性约束),因此在实现Ord类型类实例时,必须首先实现Eq的实例。

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a
  {-# MINIMAL compare | (<=) #-}

Ord类型类中内置了许多函数,但根据尾行提示,只需要实现compare函数或者(<=)函数即可,其他函数可以根据用户实现的函数和Eq类型类的约束导出。

有界类型类 Bounded

有界类型类比较简单,用于为类型提供一个最小值和最大值。

class Bounded a where
  minBound :: a
  maxBound :: a
  {-# MINIMAL minBound, maxBound #-}

Bounded类型类的实例需要对两个函数全部实现。

枚举类型类 Enum

Enum类型类定义如下:

class Enum a where
  succ :: a -> a
  pred :: a -> a
  toEnum :: Int -> a
  fromEnum :: a -> Int
  enumFrom :: a -> [a]
  enumFromThen :: a -> a -> [a]
  enumFromTo :: a -> a -> [a]
  enumFromThenTo :: a -> a -> a -> [a]
  {-# MINIMAL toEnum, fromEnum #-}

一般地,只需要定义toEnumfromEnum即可实现枚举类型的实例,这两个函数将实例中实现的类型与整数做了一一对应。

提示: 前面使用过的语法糖..(如[1..5]表示[1,2,3,4,5])就是依靠Enum类型类实现的,只要一个类型被实现为Enum的实例,就可以使用..生成一个枚举列表。

索引类型类 Ix

Ix类型类为离散有序的类型提供索引,定义如下:

class Ord a => Ix a where
  range :: (a, a) -> [a]
  index :: (a, a) -> a -> Int
  GHC.Arr.unsafeIndex :: (a, a) -> a -> Int
  inRange :: (a, a) -> a -> Bool
  rangeSize :: (a, a) -> Int
  GHC.Arr.unsafeRangeSize :: (a, a) -> Int
  {-# MINIMAL range, (index | unsafeIndex), inRange #-}

Ix类型类的最小实现为rangeindexunsafeIndex之一,以及inRange函数。其中range函数接受一个二元组(左边应当小于右边),并返回一个以元组元素为范围列表(包含元组中的两个元素);index函数用于检索某个元素位于元组标示的范围的位置,当检索元素不再范围内时会报错; unsafeIndexindex不会报错的版本,但无法保证元素是否在范围内;inRange则判断元素是否在范围内。其余函数根据名称和类型签名也不难判断其含义。

可显示类型类 Show

在Haskell中,不是所有类型“天生”就可以输出到终端上的,这需要通过实现Show类型类的实例来完成。

class Show a where
  showsPrec :: Int -> a -> ShowS
  show :: a -> String
  showList :: [a] -> ShowS
  {-# MINIMAL showsPrec | show #-}

为了能够显示某个类型的值,可以选择实现show函数或者showsPrec函数,其中showsPrec函数返回的类型ShowS实际上是String -> String的别名。

可读类型类 Read

Read类型类与Show类型类可以认为是互为相反的操作,其将字符串转换为特定类型的值。

class Read a where
  readsPrec :: Int -> ReadS a
  readList :: ReadS [a]
  GHC.Read.readPrec :: Text.ParserCombinators.ReadPrec.ReadPrec a
  GHC.Read.readListPrec :: Text.ParserCombinators.ReadPrec.ReadPrec
                             [a]
  {-# MINIMAL readsPrec | readPrec #-}

这里只介绍一个常用的必要实现函数readsPrec,其接受的Int参数为构造器的优先级(函数应用的优先级为10),返回类型中的ReadS aString -> [(a,String)]的别称。

字符串类型类 IsString

IsString类型类位于Data.String模块。在实际编程中,默认的字符串String类型某些操作效率不高(单向链表),因此Haskell还提供了诸如ByteString、Text等类型,因此有时需要将String与其他存储字符串的类型进行转换,这是就需要用到IsString类型类。

补充: ByteString通过Word8数组存储字符串,Text将ByteString编码为如ASCII,UTF8等标准格式。

class IsString a where
  fromString :: String -> a
  {-# MINIMAL fromString #-}

提示: 一般情况下,可以使用{-# LANGUAGE OverloadedStrings #-}让不同“字符串”之间进行转换。

(各种)函子类型类

函子类型类比较复杂,因此我们将其单独书写为一章函子专题,读者可以选择详细了解各种函子类型类后继续本章的其余内容,或者大致了解该部分后直接向下阅读。

派生

派生与自动派生

派生使用deriving关键字,其声明应当跟随类型的定义。

对于 普通(stock) 类型类(EqOrdEnumIxBoundedReadShow),Haskell允许用户使用自动派生出这些类型类的有关实例。如前面我们在定义Figure''SolidFigure时就自动派生了Show类型类。

默认实现的派生

有时我们希望自定义的类型类能够类似普通类型类一样可以自动派生出实例,这可以通过在类型类定义中添加默认的实现来实现。

我们修改Judgeable类型类为带有默认实现的版本Judgeable'

-- code4.hs
class Judgeable' a where 
  judgeType' :: a -> String 
  judgeType' x = "A Default Judgeable Type"
  tellShape' :: a -> String 
  tellShape' x = "A Default Judgeable Shape"

一般地,我们可以使用空的instance声明来生成Judgeable'关于SolidFigure的实例。

-- code4.hs
instance Judgeable' SolidFigure

或者,我们可以使用{-# LANGUAGE DeriveAnyClass #-}扩展,这将允许我们像自动派生普通类型类一样派生带有默认实现的类型类,这里定义SolidFigure'作为SolidFigure的副本来展示这一用法。

-- code4.hs
{-# LANGUAGE DeriveAnyClass #-}

data SolidFigure' = 
  Sphere' {getDiameter :: Diameter}
    | Cuboid' {getLength :: Length, getWidth :: Width, getHeight :: Height}
    | Cylinder' {getDiameter :: Diameter, getHeight :: Height}
  deriving (Show,Judgeable')

尝试使用Judgeable'类型类中的函数:

Prelude> tellShapeInfo' x = "This is " ++ tellShape' x ++ " which is " ++ judgeType' x
Prelude> tellShapeInfo' (Sphere 1)
"This is A Default Judgeable Shape which is A Default Judgeable Type"

新类型的派生

在前面章节我们提到了newtype定义新类型的方法,并简述了其在构造方面的限制和为编译器带来的优势。

在编译器层面上,使用newtype声明的新类型与原类型是不被区分的,当原类型已经在特定类型类中实现了实例,我们很有可能需要新类型上也有同样的实现。Haskell提供了{-# LANGUAGE GeneralizedNewtypeDeriving #-}扩展以便导出类型类。

-- code4.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}

newtype Fig = Fig SolidFigure deriving newtype (Judgeable)

提示:这里另外使用了{-# LANGUAGE DerivingStrategies #-}扩展,并在deriving后添加newtype关键字,其原因是为了消除来自DerivingAnyClass扩展的冲突,在仅使用新类型派生扩展时只需按照通常派生的写法即可。有关DerivingStrategies的内容将在下文讲解。

Prelude> tellShapeInfo x = "This is " ++ tellShape x ++ " which is " ++ judgeType x
Prelude> tellShapeInfo (Fig (Sphere 1))
"This is A Sphere which is A SolidFigure"

派生策略

派生策略扩展{-# LANGUAGE DerivingStrategies #-}用于消除来自DeiriveAnyClass和Generalized NewtypeDeriving以及内置导出之间的冲突。例如对一个新类型派生实例,到底是按照类型类中默认实现去派生还是参照与之同构的原类型派生。

使用派生策略扩展后,通过使用三个新的关键字跟随deriving对派生方式进行区分,它们分别为stock,newtypeanyclass。其中stock用于对于普通类型类(即EqOrdEnumIxBoundedReadShow)的派生;newtype用于对新类型的派生;anyclass用于含有默认实现的自定义类型类的派生。

补充: Haskell 提供了一些扩展用于自动派生其他内置的类型类,这些扩展分别为 DeriveGeneric,DeriveFunctor,DeriveDataTypeable,DeriveFoldable,DeriveTraversable,DeriveLift,启用扩展后可以就使用stock关键字对相应的类型类进行派生。另外,这些扩展已经包含在新的标准中ghc 2021中,无需手动开启扩展。

提示:上述各种扩展对应的类型类会在本章后续讲解

孤立派生

通常情况下,deriving 导出实例必须跟随类型的定义声明的,但在实际场景中,我们很难保证后期不添加新的派生,这将导致对原代码的修改,破坏了工程中的开闭原则。使用{-# LANGUAGE StandaloneDeriving #-}扩展,可以声明孤立的派生,使得派生与类型定义分离。

-- code4.hs
{-# LANGUAGE StandaloneDeriving #-}

deriving instance Eq Figure''
deriving instance Eq SolidFigure
deriving instance Eq SolidFigure'

这里为之前声明的三个类型另外派生Eq实例,这样就可以对每种类型中的值进行比较:

Prelude> Circle'' 1 == Circle'' 1
True
Prelude> Sphere 1 /= Cuboid 1 2 3
True
Prelude> Sphere' 2 == Cuboid' 3 4 5
False

特殊用法

多参数类型类

通常,类型类的声明只能包含一个参数,或者说一个类型类的实例只能属于一个类型。通过使用扩展{-# LANGUAGE MultiParamTypeClasses #-}允许类型类突破这一限制。

Figure''SolidFigure属于不同类型,即使我们派生出两个类型的Eq实例,也是无法进行两个类型之间值的比较的。使用多参数类型类扩展,可以定义一个新的类型类达到此效果。

-- code4.hs
{-# LANGUAGE MultiParamTypeClasses #-}

class Same a b where
  same :: a -> b -> Bool 
  same _ _ = False

instance Same Figure'' SolidFigure  

instance Same SolidFigure Figure'' 

instance Same SolidFigure SolidFigure where 
  same = (==) 

instance Same Figure'' Figure'' where 
  same = (==)

我们定义了一个Same类型类,内部含有一个函数same用于判断两个值是否相等,默认情况下是不相等的,因此对于两个不同的类型,直接使用空instance声明实例。对于两个相同的类型,same函数作用与(==)函数相同。

Prelude> same (Circle'' 1) (Sphere 1)
False
Prelude> same (Sphere 1) (Circle'' 1)
False
Prelude> same (Sphere 1) (Sphere 1)
True

无参数类型类

无参数类型类需要扩展{-# LANGUAGE NullaryTypeClasses #-},但目前该扩展已经被遗弃,该扩展的功能被包含到MultiParamTypeClasses中。无参数类型类可用于记录类型签名中的某些假设或添加一些全局可配置的设置在程序中[2]

-- code4.hs

class DefaultVal where 
  unit :: Double
  unit = 1
  info :: String 
  info = "global information"
  needinstantiation :: ()

显然,无参数类型类由于没有参数,无法为特定类型声明实例,因此至多为其声明一个实例。

-- code4.hs

instance DefaultVal where
  needinstantiation = ()

灵活的实例声明

当我们需要声明具有多态类型的类型类实例时,其与通常的声明方法无异。但对于嵌套的类型(即多态类型中参数实例化,如[Char]Maybe Int等)需要使用灵活的实例声明扩展{-# LANGUAGE FlexibleInstances #-}进行类型类实例的声明。

-- code4.hs
{-# LANGUAGE FlexibleInstances #-}

class Empty a where 
  isEmpty :: a -> Bool
  getFirstElem :: 

-- ordinary instance 
instance Empty (Maybe a) where 
  isEmpty Nothing = True 
  isEmpty (Just _) = False 

-- flexible instance
instance Empty [Char] where 
  isEmpty [] = True
  isEmpty (x:xs) = False

上述示例定义了一个类型类用于判断容器是否为空,第一个实例声明的类型是多态类型Maybe a,因此直接声明即可;第二个实例声明的类型是[Char],列表[a]中的a实例化为了Char,因此需要使用灵活实例声明扩展才可以。

灵活的上下文

一般地,使用类型类对类型参数进行约束具有一定限制,其只能是简单的类型变量;通过使用扩展{-# LANGUAGE FlexibleContexts #-},可以解除这一限制,以便生成更复杂的类型约束的上下文。例如Eq [a] => ...或者Ord (T a ()) => ...在开启扩展下都是合法的[3]

补充: 无论是FlexibleInstances还是FlexibleContexts扩展,实例声明都必须遵循相应的终止规则限制,如果满足这种终止规则,我们称实例约束上下文 Paterson小于 实例声明头;这种限制可以通过扩展UndecidableInstances解除。详情可参考Instance termination rules

别名的实例导出

一般地,我们无法为类型的别名声明类型类实例。但使用特别的扩展{-# LANGUAGE TypeSynonymInstances #-}可以允许这一点。

-- code4.hs
{-# LANGUAGE TypeSynonymInstances #-}

type SynFig = Fig 

instance Judgeable' SynFig  

例如,我们声明了上一节中Fig类型的一个别名SynFig,其已经派生了Judgeable类型类的实例,因此我们实现其别名的Judgeable'类型类的实例。

Prelude> tellShapeInfo' x = "This is " ++ tellShape' x ++ " which is " ++ judgeType' x 
Prelude> tellShapeInfo' (Fig (Sphere 1))
"This is A Default Judgeable Shape which is A Default Judgeable Type"

函数依赖

函数依赖通常用来限制类型类的参数,尤其是对于多参数类型类中参数之间的决定关系[4]。这种决定关系(或者说映射),使得其中的某些参数取决于另一些参数,例如对于a -> b这种关系,当a确定时,b有唯一的类型与之对应。

考虑不使用函数依赖的情况:

-- code4.hs
{-# LANGUAGE FunctionalDependencies #-}

class WithoutDep a b c where 
  func :: a -> b -> c 

instance WithoutDep Int Int Int where 
  func = \ x y -> 0

instance WithoutDep Int Int Double where
  func = \ x y -> 0

可以看到WithoutDep类型类中类型参数没有依赖关系,因而可以声明WithoutDep Int Int IntWithoutDep Int Int Double两个实例。 在实际使用中,当我们尝试使用func函数时,程序会困惑应当使用哪个实例中的函数,除非我们手动给出返回类型。

提示: 即使只声明一个实例,程序仍然无法计算出结果,直到我们给出返回类型,因此这种歧义产生的原因并不是多个实例的导致的,而是无法进行类型推断

Prelude> func 1 2
<interactive>:1:1: error:
     Could not deduce (WithoutDep a0 b0 c)
      from the context: (WithoutDep a b c, Num a, Num b)
        bound by the inferred type for ‘it’:
                   forall a b c. (WithoutDep a b c, Num a, Num b) => c
        at <interactive>:1:1-8
      The type variables ‘a0’, ‘b0’ are ambiguous
     In the ambiguity check for the inferred type for ‘it’
      To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
      When checking the inferred type
        it :: forall a b c. (WithoutDep a b c, Num a, Num b) => c
Prelude> (func (1 :: Int) (2 :: Int)) :: Int 
0
Prelude> (func (1 :: Int) (2 :: Int)) :: Double
0.0

上述方法在一些情况下可能会作为有用的技巧,例如我们希望一个函数在接受相同的输入时,会根据我们的需要(显示给出返回类型)返回不同的结果。然而我们知道,当类型之间存在依赖关系时,或者说只需要声明一个实例时,程序仍然需要显式给出返回类型,这显然带来了不小的麻烦。

通过使用函数依赖,可以帮助程序直接推断出返回类型:

class WithDep a b c | a b -> c where 
  func' :: a -> b -> c

instance WithDep Int Int Int where 
  func' = \ x y -> 0

{-
-- illegal because a definition has already existed.
instance WithDep Int Int Double where 
  func' = \ x y -> 0
-}

WithDep类型类中,第三个参数受到前两个参数的限制,因此只能声明一个WithDep Int Int Int实例,WithDep Int Int Double实例与函数依赖的唯一性不符,因而是非法的。

Prelude> func' (1 :: Int) (2 :: Int)) 
0

可见,使用函数依赖后,返回类型c被自动推断成了Int,而无需我们手动指定。

存在类型

此前我们定义的所有类型中,所有类型有关参数必须体现在等式的左侧,当参数取不同的类型时,总的嵌套类型被认为是不同的类型,这有时会带来不便。

例如,我们希望存储不同类型的值到容器中(也称异构的),并且要求这个容器的长度是可变化的。单纯地使用列表是无法实现的,对于列表[a],其存储元素的类型必须为a所对应的类型。

-- ['a',1,True] 
-- impossible because of type checking

最简单直接的方法是使用代数数据类型,将不同的类型作和运算生成一个新的类型,其中每个构造函数代表一个类型[5]

-- code4.hs
data HeteData =
  HeteInt Int
  | HeteChar Char
  | HeteBool Bool

这样,就可以存储“不同”类型的数据了:

-- code4.hs
a :: [HeteData]
a = [HeteChar 'a',HeteInt 1,HeteBool True]

但是这种方法只限于表示有限的类型,不适合对于有扩展需要的情况。

在Haskell中的Data.Dynamic库中,提供了一种动态类型,它允许直接对不同类型进行封装,以存储“不同”类型的数据:

-- code4.hs
import Data.Dynamic

b :: [Dynamic]
b = [toDyn 'a',toDyn (1 :: Int),toDyn True]

geta :: Char
geta = case (fromDynamic . head) b of
  Nothing -> error "Type mismatch"
  Just x -> x

或者,第三种方法就是使用存在类型扩展,通过使用扩展{-# LANGUAGE ExistentialQuantification #-},可以在等式右侧对类型参数进行限制,而无需在等式左侧声明它们。

-- code4.hs

data ExistData = forall a. (Show a) => ExistHeteData a 

c :: [ExistData]
c = [ExistHeteData 'a',ExistHeteData (1 :: Int),ExistHeteData True]

showc :: String
showc = show c'
  where f (ExistHeteData t) = show t
        c' = map f c

示例中,ExistData类型中类型参数a受到Show类型类的限制,即任何值ExistHeteData a中的a的类型必须是Show类型类的实例。这样我们就能够构造出存储异构数据的列表,showc利用Show类型类的限制,使用show函数将列表中存储的异构数据转换为字符串。

补充: 细心的读者可能希望尝试对ExistData类型构造类似geta函数,但这是无法通过类型检验的(思考这是为什么?),因为在这个类型中的类型参数被隐藏起来。一种解决方案是使用Typeable类型类中的cast函数(这里只作为了解,不过多讲解)

关联类型

关联数据类型(也称为类型族,是支持数据类型的临时重载的扩展),是类型层面上的函数,可以作为函数依赖的替代方法[5]

提示:实际上,类型族的表现形式有两种,关联类型是其一,用于类型类中;另一种出现在顶层,这里不做讨论。更多有关类型族的内容请详见有关章节。

-- code4.hs
{-# LANGUAGE TypeFamilies #-}

class Assoc1 a b where
  type FuncType a b :: * 
  assocfunc :: a -> b -> FuncType a b

instance Assoc1 Int Int where 
  type FuncType Int Int = Int 
  assocfunc a b = 0

这里,FuncTypeab的类型映射到一个新的类型上,这个类型是唯一的,即FuncType a bab决定。

实例重叠

目前有关实例重叠的处理扩展已经被弃用,详情可以参考Overlapping Instances

其他常用类型类

半群类型类 Semigroup

半群类型类对应数学中的 半群(semigroup)。对于一个集合\( S \)以及一个作用在该集合上的二元运算\( \cdot \),满足封闭性和结合性,则称这个数学结构为半群。

class Semigroup a where 
  (<>) :: a -> a -> a
  GHC.Base.sconcat :: GHC.Base.NonEmpty a -> a
  GHC.Base.stimes :: Integral b => b -> a -> a
  {-# MINIMAL (<>) #-}

Semigroup类型类中,(<>)即为对应的二元运算,也是半群实例的最小实现。

提示:好奇的读者可以使用hoogle查询另外两个函数的作用,这里不再赘述

我们已经接触过许多满足半群的数学结构:

newtype BoolA = BoolA Bool 

newtype BoolO = BoolO Bool 

instance Semigroup BoolA where 
  BoolA a <> BoolA b = BoolA (a && b)
 
instance Semigroup BoolO where 
  BoolO a <> BoolO b = BoolO (a || b)


{- 已经内置的实例
instance Semigroup [a] where 
  [] <> ys = ys 
  (x:xs) <> ys = x : (xs <> ys)
-}

在上面的示例中,布尔类型作为集合,与运算和或运算均可以作为满足半群的二元运算符,但由于我们无法为同一类型声明两个完全相同的类型类的实例,这里使用newtype将创建两个与布尔类型同构的新类型。另外,Haskell中已经内置了半群关于列表的实例,其中二元运算符为列表的拼接操作。

幺半群类型类 Monoid

当半群中存在单位元\( 1 \),即任何元素与其作运算都是该元素本身,则称这个半群为幺半群。

class Semigrop a => Monoid a where 
  mempty :: a 
  mappend :: a -> a -> a
  mconcat :: [a] -> a 
  {-# MINIMAL mempty #-}

Monoid类型类中,mempty表示幺元(单位元),mappend表示二元运算。

Semigroup中的举出的三个示例,同时也是幺半群:

-- code4.hs

instance Monoid BoolA where 
  mempty = BoolA True 
 
instance Monoid BoolO where 
  mempty = BoolO False

{- 已经内置的实例
instance Monoid [a] where 
  mempty = []
-}

对于布尔类型分别和与运算以及或运算构成的半群,TrueFalse分别为其单位元,因此它们都是幺半群。对于列表来说,显然空列表关于拼接运算是单位元,因此列表和拼接运算以及空列表也构成了幺半群。

默认值类型类 Default

顾名思义,Default类型类提供某一类型的默认值。它位于data-default库中。

class Default a where 
  def :: a 

可折叠类型类 Foldable

Foldable类型类用来表示具有容器的特性,所有实现该类型类实例的类型,都可以折叠为一个值,例如前面学习的列表就是可折叠的。

class Foldable (t :: * -> *) where 
  Data.Foldable.fold :: Monoid m => t m -> m
  foldMap :: Monoid m => (a -> m) -> t a -> m
  foldr :: (a -> b -> b) -> b -> t a -> b
  Data.Foldable.foldr' :: (a -> b -> b) -> b -> t a -> b 
  foldl :: (b -> a -> b) -> b -> t a -> b
  Data.Foldable.foldl' :: (b -> a -> b) -> b -> t a -> b 
  foldr1 :: (a -> a -> a) -> t a -> a 
  foldl1 :: (a -> a -> a) -> t a -> a 
  Data.Foldable.toList :: t a -> [a]
  null :: t a -> Bool 
  length :: t a -> Int 
  elem :: Eq a => a -> t a -> Bool 
  maximum :: Ord a => t a -> a
  minimum :: Ord a => t a -> a
  sum :: Num a => t a -> a
  product :: Num a => t a -> a 
  {-# MINIMAL foldMap | foldr #-}

Foldable类型类中最小实现为foldMapfoldr,其中foldMap将一个容器中的元素全部进行映射(要求映射后的对象是幺半群),并使用幺半群中的二元运算将所有映射后的元素折叠为一个值;foldr接受一个二元函数,一个初始值和一个容器,该函数不断使用二元函数作用在初始值和容器中的元素并生成新的初始值,直到容器为空时,返回折叠后的值(即此时的初始值)。

提示:带有'标记的折叠函数是严格求职版本,其他的函数通过名称和类型签名不难理解其含义,这里不再赘述。

补充:可使用DeriveFoldable扩展自动派生Foldable实例

可游历类型类 Traversable

Traversable类型类将具有可折叠特性的特殊的类型继续进行封装,得到一些共有的计算,该类型类比较复杂,且涉及到函子有关概念,这里仅给出定义和一个示例以供学有余力的读者了解。

class (Functor t,Foldable t) => Traversable t where 
  traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
  sequenceA :: Applicative f => t (f a) -> f (t a)
  mapM :: Monad m => (a -> m b) -> t a -> m (t b)
  sequence :: Monad m => t (m a) -> m (t a)
  {-# MINIMAL traverse | sequenceA #-}

最小实现是traversesequenceA函数,下面给出使用这两个函数的示例:

Prelude> traverse (\t -> Just 'a') [1,2,3,4]
Just "aaaa"
Prelude> sequenceA [Just 1,Just 2,Just 3,Just 4]
Just [1,2,3,4]

补充: 可使用DeriveTraversable扩展自动派生Traversable实例

数类型类 Num

Num类型类用于具有数字特性的类型,其定义如下:

class Num a where 
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a 
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  {-# MINIMAL (+), (*), abs, signum, fromInteger, (negae | (-)) #-}

[1] Polymorphism. (2015, January 21). HaskellWiki, . Retrieved 02:37, April 10, 2024 from https://wiki.haskell.org/index.php?title=Polymorphism&oldid=59216.

[2] Language options. Glasgow Haskell Compiler 8.6.5 User's Guide. Retrieved 10:06, April 13, 2024 from https://downloads.haskell.org/~ghc/8.6.5/docs/html/users_guide/glasgow_exts.html#nullary-type-classes

[3] Language options. Glasgow Haskell Compiler 8.6.5 User's Guide. Retrieved 16.03, April 15, 2024 from https://downloads.haskell.org/~ghc/8.6.5/docs/html/users_guide/glasgow_exts.html#extension-FlexibleContexts

[4] Functional dependencies. (2021, July 21). HaskellWiki, . Retrieved 03:35, April 13, 2024 from https://wiki.haskell.org/index.php?title=Functional_dependencies&oldid=64590.

[5] GHC/Type families. (2023, February 4). HaskellWiki, . Retrieved 01:16, April 15, 2024 from https://wiki.haskell.org/index.php?title=GHC/Type_families&oldid=65516.