skyscribe.programming.thinking

汇小流以成江海,积跬步以至千里

Haskell Typeclass

| Comments

Haskell 中也有class关键字,但其目的却和 OO 中的类有着巨大的差别。在 OO 世界中,类用来描述一大堆具有共同数据和行为的对象的抽象;而 Haskell 中的 class 则是用于抽象提供同样函数接口的数据类型。每一个 ADT 都可以用instance来生命其满足某个 class 并且给出对应于具体函数的实现,即 class 在 Haskell 中实际用于约束数据类型,因而又被成为 typeclass.

typeclass 的作用

由于 Haskell 是一个纯函数式语言,所有的操作都是用函数方式实现的(递归和模式匹配);同时作为一个强类型语言,所以函数的参数必须绑定于特定的类型,而不同的数据类型之间是不能直接转换的 ( 需要转的也必须通过某些函数来实现 ), 那么对于同样一个类似的函数,可能就需要对不同的类型有不同的实现,因为操作的类型可能不同,这样就会带来很繁琐的代码,例如:

1
2
3
someOpOnInt::Int -> Int
someOpOnDouble::Double->Int
someOpOnFractional::Fractional-> Int

由于他们所做的操作本质上相同,但是由于强类型系统的制约,编写者必须给出不同的函数名来对应于不同的类型,后果就是重复的代码乃至糟糕的代码质量。另一个可能的问题则是对于每一个新定义的类型,必须定义一个新的函数。其它的模块想要调用此功能也必须针对不同的类型做不同的处理,导致代码不能重用。

Typeclass则可以很好的解决这个问题: – 一个 typeclass 来定义所支持的操作,例如

1
2
class SomeOp a where
    someOp :: a -> Int
  • 每一个可以支持该操作的类型可以实现对应的操作,如:
1
2
3
4
5
6
7
8
9
10
11
instance SomeOp Int where
    -- implementition for Int type
    someOp x = undefined

instance SomeOp Double where
    -- implementation for Double
    someOp x = undefined

instance SomeOp Fractional where
    -- implementation for Fractional
    someOp x = undefined
  • typeclass是开放的,这意味着你可以在不同的模块里边实现其它模块中定义的 typeclass
1
2
3
4
5
6
data BrandNewType = BrandNewType String Int
        deriving (Show, Eq)

instance SomeOp BrandNewType where
    -- imp for new type
    someOp x = undefined

由于 typeclass 实现的是对于 type 的抽象,如果熟悉 C++ 的模板系统和被 C++ 被拖出新标准的 concept 概念,那么我们就容易发现 typeclass 和模板系统有很多的相似基因。而 typeclass 能够更优雅的抽象类型接口,则得益于 Haskell 的强类型系统了。想想 C++ 中隐式类型转换给模板实现带来的困扰,Haskell 的 typeclass 是一种更优雅的抽象。

Read 和 Show

这是两个系统预定义的 typeclass, Show 用于将某个类型转换为 string, 而 Read 则用于从一个字符串表述中构造一个指定类型的数据。二者结合可以完成数据的序列化和反序列化。系统提供的 putStrLn 操作于某个数据类型的时候,如果其类型继承了 Show,那么它的字符串表示就会被打印出来。当然show 函数也可以用于打印其字符串表述,而 Read 则用构造出一个指定类型的对象,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
data Color = Read | Green | Blue
instance Show Color where
    show Red = "Red"
    show Green = "Green"
    show Blue = "Blue"

instance Read Color where
    readsPrec _ value =
        tryParse [("Red", Red), ("Green", Green), ("Blue", Blue)]
        where tryParse [] = []
              tryParse ((attempt, result) : xs) =
                if ( take (length attempt) trimed) == attempt
                then [(result, drop (length attempt) trimed)]
                else tryParse xs
              where trimed = lTrim value

lTrim (' ':xs) = lTrim xs
lTrim other = other

-- test in ghci
*Main> let inst = [Red, Blue, Green]
*Main> show inst
"[Red,Blue,Green]"
*Main> let inst' = read (show inst) :: [Color]
*Main> inst'
[Red,Blue,Green]

这里对于 list 类型的处理可以被系统自动推倒出来。

系统预定义的 typeclass

Haskell 标准规定编译器需要预定义一些基本的 typeclass, 并且对于系统预定义的数据类型,编译器也给出了对应的实现。这些预定义 typeclass 包括: – Read – 数据反序列化
– Show – 数据序列化
– Ord – 排序支持,描述顺序关系
– Enum – 枚举接口
– Eq – 相等关系

对于自己定义的 ADT, 这些 typeclass 可以用 deriving 的方式交给Haskell编译器来自动推导,省却了诸多麻烦。限制条件是,我们自己用data声明的ADT类型必须满足: 其中引用的类型必须也满足需要derive的typeclass,这些类型可以是手动声明的方式满足typeclass.

如下的例子就是一个例外情况:

1
2
3
4
5
6
7
8
9
10
11
data CannotShow = CannotShow
    deriving (Show)

data CannotDeriveShow = CannotDeriveShow CannotShow
    deriving (Show)

-- this will work
data OK = OK
instance Show OK where show _ = "OK"
data ThisWorks = ThisWorks OK
    deriving (Show)

这里的第一个例子中,引用的类型没有去指明继承 typeclass 因而会导致编译失败,而第二个类型则可以。

问题 – overlapping

由于 typeclass 是开放的, 不同的模块可能对不同的类型提供不同的 typeclass instance实现,二者就可能出现冲突,例如:

1
2
3
4
5
6
7
8
9
10
11
class Borked a where
    bork:: a -> String

instance Borked Int where
    bork = show

instance Borked (Int, Int) where
    bork (a,b) = bork a ++ ", " ++ bork b

instance (Borked a, Borked b) => Borked (a,b) where
    bork (a,b) = ">>" ++ bork a ++ ", " ++ bork b ++ "<<"

如果我们需要调用bork (1,2), 这里 haskell 编译器没法自动判断该选择那一个实现,因为最后两个都同样满足instance条件。GHC 中可以通过扩展 OverlappingInstance 来解除这一限制,引导编译器选择最具体的类型实现。

Comments