Haskell 自定义type与typeclass

2024-09-02 16:18:41 浏览数 (2)

前言

在看《Haskell趣学指南》这本书的Build Our Own Type and Typeclass一章时,不是很好理解,这里结合《Real World Haskell》这本书做一下记录。

自定义type

Part One

Haskell中使用data关键字来定义新的数据类型:

代码语言:javascript复制
data BookInfo = Book Int String [String] deriving (Show)

那么如何解读上面的表达式呢? 首先data关键字后边的BookInfo是新类型的名字,我们称BookInfo为*类型构造器*。类型构造器用于指代(refer)类型。类型名字的首字母必须大写,因此类型构造器的首字母也必须大写。 接下来的Book是*值构造器*(或者称:*数据构造器*)的名字,类型的值就是由值构造器创建的。 Book之后的Int String [String] 是类型的组成部分 在这个例子中,Int表示书ID, String表示书名,[String]表示作者

上面的描述其实很像OOP中的累的构造方法,BookInfo部分类似于OOP中的class,上文中的值构造器类似于class的构造方法,Book可以认为是构造方法的方法名,java等一些语言中构造方法是与class是同名的,但是Haskell中很明显没有这种约束,Haskell中类型构造器和值构造器的命名是独立的, 所以其实值构造器是可以与类型构造器同名的,即上面的例子可以写成:data BookInfo = BookInfo Int String [String]

可以将值构造器看作是一个函数:它创建并返回某个类型的值。下面的例子中我们将Int String [String] 三个类型的值应用到Book, 从而创建一个BookInfo类型的值

代码语言:javascript复制
csapp = Book 123456 "Computer Systems: A Programmer's Perspective" ["Randal E.Bryant", "David R.O'Hallaron"] 

使用 :info 命令查看更多关于给定表达式的信息

代码语言:javascript复制
:info BookInfo

类型别名

上面BookInfo类型的例子中,Int String [String] 一眼看不出来这三个成分是干什么用的,通过类型别名可以解决这个问题:

代码语言:javascript复制
type BookId Int
type BookName String
data BookInfo = Book BookId BookName [String]

这样是不是一目了然了呢。 > 跟golang中的type关键字或者c/c 中的typedef 很像

类型别名也可以有参数

代码语言:javascript复制
type AssocList k v = [(k,v)]
type IntMap v = Map Int v
type IntMap = Map Int

algebraic data type

Bool类型是代数数据类型的一个典型代表,一个代数类型可以有多个值构造器

代码语言:javascript复制
data Bool = False| True

以此为例我们可以说Bool类型由True值或False值构成 下面是《Haskell趣学指南》中的例子:

代码语言:javascript复制
data Shape = Circle Float Float Float | Rectangle Float Float Float Float

意思是图形可以是圆形或者是长方形

Record Syntax

比较好理解暂不做过多说明,后续再补坑

Type parameters

列表类型是多态的:列表中的元素可以是任何类型。我们也可以给自定义的类型添加多态性。只要在类型定义中使用类型变量就可以做到这一点。Prelude 中定义了一种叫做*Mayb*的类型:它用来表示这样一种值——既可以有值也可能空缺,比如数据库中某行的某字段就可能为空。

代码语言:javascript复制
data Maybe a = Nothing | Just a         -- Defined in ‘GHC.Maybe’

递归定义

一个代数数据类型的值构造器可以有多个field,我们能够定义一个类型,其中他的值构造器的field就是他自己,这样我们可以递归的定义下去。我们可以这样定义我们的List:

代码语言:javascript复制
data List a = Empty | Cons a (List a) deriving(Show,Read,Ord) 

用record syntax表示:

代码语言:javascript复制
data List a = Empty | Cons {headList::a, tailList::List a} deriving(Show,Read,Ord) 
代码语言:javascript复制
ghci> Empty
Empty
ghci> 5 `Cons` Empty
Cons 5 Empty
ghci> 4 `Cons` (5 `Cons` Empty)
Cons 4 (Cons 5 Empty)
ghci> 3 `Cons` (4 `Cons` (5 `Cons` Empty))
Cons 3 (Cons 4 (Cons 5 Empty))

我们可以只用特殊字符来定义函数,这样他们就会自动拥有中缀的性质,同样的我们可以套用在值构造器上,因为他们不过是回传类型的函数而已

代码语言:javascript复制
infixr 5 :-:
data List a = Empty | a :-: (List a) deriving (Show, Eq, Read, Ord)

定义函数成operator时能够同时指定fixity(不是必须的)。fixity指定了他应该是left-associative还是right-associative,还有他的优先级。infixr是右结合,infixl是左结合,infix无左右优先性。优先级0-9。例:*的fixity是infixl 7, 的fixity是infixl 6, infixl代表他们都是left-associative,但是*的优先级大于 。

这样我们就可以这样写:

代码语言:javascript复制
ghci> 3 :-: 4 :-: 5 :-: Empty
(:-:) 3 ((:-:) 4 ((:-:) 5 Empty))
ghci> let a = 3 :-: 4 :-: 5 :-: Empty
ghci> 100 :-: a
(:-:) 100 ((:-:) 3 ((:-:) 4 ((:-:) 5 Empty)))

haskell在deriving Show的时候仍然会视值构造器为前缀函数,因此要用括号括起来

构造自己的Typeclass

首先看一下Eq是怎么被定义的:

代码语言:javascript复制
class Eq a Where
    (==) :: a->a->Bool
    (/=) :: a->a->Bool
    x == y = not (x /= y)
    x /= y = not (x == y)

tip: 上面的代码是书中给出的而在ghci中打印出来实际是下面这样的: Prelude> :info Eq class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool {-# MINIMAL (==) | (/=) #-} -- Defined in ‘GHC.Classes’ instance Eq a => Eq [a] -- Defined in ‘GHC.Classes’ instance Eq Word -- Defined in ‘GHC.Classes’ instance Eq Ordering -- Defined in ‘GHC.Classes’ ...

解释下:class Eq a where代表我们定义了一个typeclass叫做Eq,a是一个类型变量,他代表任何我们在定义instance时的类型,接下来我们定义了几个函数,不一定要实现函数但一定要写出函数的类型声明。

下面看下这个类型:

代码语言:javascript复制
data TrafficLight = Red | Yellow | Green

这里定义了一个红绿灯的类型,该类型目前还不是任何class的instance。虽然通过derive可以让它成为Eq或者Show的instance,但在这里我们手动实现:

代码语言:javascript复制
instance Eq TrafficLight where
    Red == Red = True
    Green == Green = True
    Yellow == Yellow = True
    _ == _ = False

instance关键字用来说明我们定义某个typeclass的instance。

由于==使用/=来定义的,同样/=使用==定义的,所以我们只要在instance中复写其中一个就好了。我们这样叫做定义了一个minimail complete difinition。这是说能让类型符合class行为所最小实现的函数数量。而Eq的minimal complete difinition需要==或者/=实现其中一个。而如果Eq这样定义:

代码语言:javascript复制
class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool

当我们定义instance时就需要实现两个函数。所以minimal complete difinition就是==和/=。

我们再来写Show的instance,要满足Show的minimal complete difinition需要实现show函数,它接收一个值返回一个字符串

代码语言:javascript复制
instance Eq TrafficLight where
    show Red = "Red light"
    show Yellow = "Yellow light"
    show Green = "Green ligth"

subclass

可以把typeclass定义成其他typeclass的subclass,Num的class声明就有点长:

代码语言:javascript复制
class (Eq a) => Num a where
    ...

我们可以在很多地方加上类型约束,这里就是在class Num where 中的a上加上它必须是Eq instance的约束。其实这可以理解为在定义Num这个class时,必须先定义他为Eq的instance。

泛型instance

Maybe或者List这种与TrafficLight不同,Maybe是一个泛型。它接收一个类型参数(像是Int)从而构造出一个具体的类型。从Eq的typeclass的声明中可以看到a必须是一个具体的类型,而Maybe不是一个具体的类型我们不能写成这样:

代码语言:javascript复制
instance Eq Maybe where
    ...

下面的代码虽然Maybe m 是一个具体的类型但是还有一个问题,那就是无法保证Maybe装的东西可以是Eq

代码语言:javascript复制
instance Eq (Maybe m) where
    Just x == Just y = x == y
    Nothing == Nothing = True
    _ == _ = False

所以还应该加上一个类型约束:

代码语言:javascript复制
instance (Eq m) => Eq (Maybe m) where
    Just x == Just y = x == y
    Nothing == Nothing = True
    _ == _ = False

大部分情况下class声明中的类型约束都是要让一个typeclass成为另一个typeclass的subclass。而在 instance 宣告中的 class constraint 则是要表达型别的要求限制。

如果想看一个 typeclass 有定义哪些 instance。可以在 ghci 中输入 :info YourTypeClass。所以输入 :info Num 会告诉你这个 typeclass 定义了哪些函数,还有哪些类型属于这个 typeclass。:info 也可以查找类型跟类型构造器的信息。如果你输入 :info Maybe。他会显示 Maybe 所属的所有 typeclass。:info 也能告诉函数的型别宣告。

Functor typeclass

首先看下Functor这个typeclass

代码语言:javascript复制
class Functor f where
    fmap :: (a -> b) -> f a -> f b

tip: ghci 8.8.1中打印结果如下:

代码语言:javascript复制
Prelude> :info Functor
class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  {-# MINIMAL fmap #-}
        -- Defined in ‘GHC.Base’
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Functor ((->) r) -- Defined in ‘GHC.Base’
instance Functor ((,) a) -- Defined in ‘GHC.Base’

可以看到typeclass中的类型变量f并不是一个具体的类型,而是类似于Maybe这样的泛型。从上面我们可以看到fmap接收一个从a类型映射到b类型的函数和一个装有a类型值的functor,返回一个装有b类型值的functor

看下学list时学到的map函数:

代码语言:javascript复制
Prelude> :t map
map :: (a -> b) -> [a] -> [b]

它接收一个从a类型映射为b类型的函数,和一个装有a类型值的List返回一个装有b类型值的List

是不是很像fmap,不错,List正是一个Functor的instance,而map就是fmap的实现(这一点看下ghci中:info Functor的打印结果就能确认)。

同样的Maybe也是Functor的一个instance:

代码语言:javascript复制
instance Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap f Nothing = Nothing

看到这不免有些疑问,为什么上面instance Eq Maybe where不行在这里写成instance Functor Maybe where就行了呢?原因是Functor要接收的是一个泛型,而不是一个具体的类型。如果把f替换成Maybe,fmap就像是这样:(a -> b) -> Maybe a -> Maybe b,如果像上面将Eq时一样将f替换成Maybe m的话就会成这个样子了:(a -> b) -> Maybe m a -> Maybe m b 这显然是不对的。

如果一个泛型是接收两个参数的呢,以Either a b为例,可以这样写:

代码语言:javascript复制
instance Functor (Either a) where
    fmap f (Right x) = Right (f x)
    fmap f (Left x) = Left x

就是把Either a作为Functor的一个instance(Either不能作为Functor的instance)

Kind

泛型(型别构造子)接收其他类型作为它的参数来构造出一个具体的类型。这有点像函数,也是接收一个值作为参数并回传另一个值。对于类型如何被套用到泛型上,我们看下正式的定义。 像是3,"abc"或者是takeWhile的值都有自己的类型(函数也是值的一种)。类型是一个标签,值会把它带着,这样我们就能推导出它的性质。但类型也有自己的标签,叫做kind,kind是类型的类型。

我们可以在ghci中通过:k来获取一个类型的kind:

代码语言:javascript复制
Prelude> :k Int
Int :: *

*代表这个类型是具体类型。一个具体类型是没有任何类型参数的,值只能属于具体类型。*的读法叫做star或是type。

我们再看下Maybe的kind:

代码语言:javascript复制
Prelude> :k Maybe
Maybe :: * -> *

可以看到Maybe的类型构造子接收一个具体类型(像是Int)然后返回一个具体类型。就像Int -> Int代表这个函数接收Int并返回Int。* -> *代表这个类型构造子接收一个具体类型并返回一个具体类型。我们再对Maybe套用类型参数后再看看它的kind:

代码语言:javascript复制
Prelude> :k Maybe Int
Maybe Int :: *

0 人点赞