前言
在看《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
是怎么被定义的:
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这样定义:
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
代码语言:javascript复制tip: ghci 8.8.1中打印结果如下:
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
为例,可以这样写:
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:
Prelude> :k Int
Int :: *
*
代表这个类型是具体类型。一个具体类型是没有任何类型参数的,值只能属于具体类型。*
的读法叫做star或是type。
我们再看下Maybe的kind:
代码语言:javascript复制Prelude> :k Maybe
Maybe :: * -> *
可以看到Maybe的类型构造子接收一个具体类型(像是Int)然后返回一个具体类型。就像Int -> Int
代表这个函数接收Int并返回Int。* -> *
代表这个类型构造子接收一个具体类型并返回一个具体类型。我们再对Maybe套用类型参数后再看看它的kind:
Prelude> :k Maybe Int
Maybe Int :: *