一类
1 自定义类型
data关键字用来定义自己的数据类型 (类型必须大写)
data Rank=Ten | jack |queen |king |ace
2 多态
(1)backwards::[a]->[a]
backwards []=[]
backwards (h:t)=backwards t ++ [h]
[a]的类型是一个元素为任意类型的列表。
(2)data Triplet a =Trio a a a deriving (Show)
a是一个类型变量。表示具有相同类型的三元组是Triplet类型的。(deriving (Show) 用来显示数据)
3递归类型
树的定义:节点,要么是叶子,要么是树的列表。值在叶子上。
data Tree a=Children [Tree a] |Leaf a deriving (Show)
Tree是类型构造器,Children ,Leaf 是数据构造器
使用:
let tree =Children [Leaf 1,Children[Leaf 2,Leaf3] ]
tree
求树的深度:
depth(Leaf _)=1
depth(Children c)=1+max(map depth c)
4类 (类似于操作)
不同于面向对象编程中的概念,类定义了哪些操作可以在那些输入上进行。用来精细控制多态和重载。
如果类型支持类的所有函数,那么这个类型是类的一个实例。
二 monad [1]
1 定义
(1)monad 作用是 以一种特定属性的方式组合函数
(2) Haskell 中把单子定义成一个 type class:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a ->m a
对任一个型别建构元 m,如果我们能定义出 >>= 和 return 两个函数,并满足后述的一些条件,m 就是一个单子。对 type class 我们不在此解释太多,只需知道这是 Haskell允许我们重载 (overload) >>= 和 return 等符号、使许多型别的类似函数可以用同一组符号表示的机制就可以了。型别 m a 代表一个「结果为型别 a 的运算」,运算过程中可能产生单子 m希望描述的副作用,如同 Maybe a 用来表示一个结果是 a,但可能会产生例外的运算。函数return 把一个型别为 a 的值「提升」到 m a. 在 Maybe 的例子中,return 就是 Just.函数>>= 则是提升到 m 之上的函数应用,左手边是一个 m a,右手边是一个从 a 到 m b 的函数;x >>= f 大致上的意思是执行 x 代表的运算,如果得到一个型别是 a 的值,把他传给f.结果的型别是 m b.
(3)主要包括:
1 类型构造器-定义一个类型。
2 return 返回。
3 >>= 操作, 该操作 接受一个类型的实例 和 一个函数 (实例写在>>=前边,函数写在>>=后边),输出一个结果.
例如,
(>>=) : Maybe a -> (a -> Maybe b) -> Maybe b
(Just x) >>= f = f x
Nothing >>= f = Nothing
例如Maybe monad的定义,
data Maybe a=Nothing | Just a (Just是一种特定的类型)
instance Mnoad Maybe where
return =just
Nothing >>=f =Nothing
(Just x)>>= f =f x
2 单子定理
单子定律
单子的 return 与 >>= 两个函数当然不能随便写。型别建构元 m 要称为单子,return 和>>= 须满足下面三个定律:
1. (return x) >>= f == f x,
2. m >>= return == m,
3. (m >>= f) >>= g == m >>= (\x -> f x >>= g).
头两条定律谈到 return 的合理行为。第一条确保 return x 不含副作用: (return x)>>= f 应该和把 x 直接丢给 f 一样。第二条定律中,把 m 的结果直接 return 回来并不改变其值。第三条是 >>= 的递移律,可和函数组合的定义比较:
f (g x) = (f . g) x
这条定律确保 >>= 的行为确实类似函数应用。
读者可以检查一下 Maybe 的 return 与 >>= 确实满足这三条定律。目前 Haskell 本身无法自动检查这三条定律,只能希望程序员遵守。程序员照理说也希望所有单子都满足这些定律,以便于程序的推理分析。不幸的是确实有些很有用的「单子」为了效率因素会在某些情况下违反单子定律。以后有机会再谈。
3 列表单子我们进一步扩充算式,添加一个 Or
操作数:
data Expr = Num Int | Neg Expr | Add Expr Expr
| Div Expr Expr | Or Expr Expr
此处的Or
倒不是逻辑上的「或」,而是非确定(non-deterministic)运算:Or
e1 e2
的值可能是 e1
,也可能是 e2
.非确定操作数有点牵强地用在这里仅是为了举例,但在程序语言的应用中,需要处理非确定性的情形并不少见。例如一个文法
parse一段句子,可能有不只一种 parse的方法。我们会需要写程序找出所有 parse.
我们希望找出一个算式所有可能的值,因此 eval
的型别改为 Expr->
[Int]
,将所有可能的值放在一个串行中。串行是另一个单子的常见例子。函数 return
将一个型别为 a
的值 x
提升成串行,自然的选择是传回 [x]
,因为 x
是自己的唯一值:
return :: a -> [a]
return x = [x]
假设 xs
是一个型别为 [a]
的串行,函数 f
拿一个 a
,将所有的可能结果放在型别为 [b]
的串行中。我们如何把 xs
喂给 f
呢?答案是先用 f
处理 xs
里的每个元素,再把结果接起来:
(>>=) :: [a] -> (a -> [b]) -> [b]
xs >>= f = concat (map f xs)
写成 list comprehension可能更清楚: xs >>= f = [y | x <-xs, y <- f x]
.注意 return
和 >>=
的型别分别是 a->
[a]
和 [a]-> (a -> [b]) -> [b]
,符合 Monad
class的要求。(此处为了说明而标上型别。在
Haskell中,由于型别已在 class宣告中给过一次,在此是不加型别的。)
函数 eval
该怎么处理新加的 Or
呢?Ore1
e2
的所有可能值是 e1
的所有可能值以及e2
的所有可能值,所以:
eval (Or e1 e2) = eval e1 ++ eval e2
而对于 Num
, Neg
, Add
等情况,很幸运地,eval
可在不修改的情况下直接使用新的 return
与 >>=
定义。例如
eval (Add (Or (Num 1) (Num 3)) (Or (Num 3) (Num 4)))
的值会是 [4,5,6,7]
.
至于 Div
呢?函数 eval
的 Div
条款用了一个 mzero
,当时的值是 Nothing
.在此处只要把mzero
设定为 []
就可以了
-- 分母为零时 Div
没有值,因此我们传回空串行。
串行单子在此处用来表达非确定性计算,有时当我们说「非确定性单子」时,指的是串行。在 Haskell这种缓式语言中,串行尾端在需要时才会算出来,因此 eval
的行为像是回溯
(backtracking):先用第一个值去试试看,如果可用就一直使用下去,如果失败则回溯回来换成下一个值。读者可以试试看 eval(Div (Num 4) (Add (Neg (Num 2)) (Or (Num 0) (Num 2))))
(4
/ (-2 + (0 OR 2))) 是怎么算出来的。有人也因此把串行称作「回溯单子」。但不论非确定性或回溯还另有其他数据结构可表示(有的也许会特别强调公平性或效率等等因素)。此处我们还是简单地称之为串行单子。
参考 [1] http://www.iis.sinica.edu.tw/~scm/ncs/2009/11/a-monad-primer/
参考 [2]http://cookoo.iteye.com/blog/27006
参考 [3]http://zhiwei.li/text/2011/05/haskell%E7%9A%84monad/