第四章 The monad laws

关于 Monad 的数学基础,请看这里: http://www.javaeye.com/topic/147443
本章理论性较强,在此感谢T1老大的深入讲解,及 st_monad的介绍。

The three fundamental laws
Failure IS an option
No way out
Zero and Plus
Summary

这份教程到现在为止一直避免纯技术的讨论,不过有一些monad的技术点必须说。Monad必须遵从一些“Monad公理”。Haskell编译器并不保证这些公理,必须由程序员来保证他们声明的Monad实例遵从这些法则。Haskell中的Monad类还包括了一些我们没看到超出最小完整monad定义的功能。有很多Monad遵守标准monad规则之外的附加规则,有一个附加的Haskell类来支持这些扩展的Monad.

The three fundamental laws

Monad的概念来自一个叫“范畴论”的数学分支。 尽管如此,我们创建与使用monad并不一定要知道范畴论,我们只需要遵从很少的一些数学形式。要创建一个monad,不只是在Haskell中声明一个带有正确类型原形的Monad类的实例。要成为一个合适的monad, return 和 >>= 函数必须按如下三个规则一起工作:

	(return x) >>= f == f x 
	m >>= return == m 
	(m >>= f) >>= g == m >>= (\x -> f x >>= g) 

(借用 st_monad 的说法:

  • 第一个,monad bind到constructor等价于直接apply monad中的pure部分到constructor。
  • 第二个,return保留monad的所有信息不变。
  • 第三个,bind运算满足结合律。
  • 规则1规定return作为>>=的左单位元,规则2规定return作为>>=的右单位元;规则三规定了bind 运算的结合律。满足这三条规则可以保证使用monad的do形式在语义上的一致性。

    任何一个带有return和bind操作符并满足这三条规则的类型构建子就是一个monad。在Haskell中,编译器不会检查Monad类的每个实例是否遵守这些规则。这要由程序员来保证他创建的每一个Monad实例都满足这些规则。

    Failure IS an option

    之前给出的Monad类定义只是一个最小完备实现。实际上完整的Monad类定义还包括两个附加的函数: fail 和 >>。
    默认的 fail 函数实现是:

    fail s = error s 
    

    你的monad不需要改变这个定义,除非你想给失败提供不同的行为或者把失败混合进你的monad的计算策略中。比如Maybe monad,把 fail 定义为:

    fail _ = Nothing 
    

    这样在Maybe monad 中当它与其他函数 bind 时,fail 将返回一个有意义的 Maybe monad的实例。(?这句翻的很别扭,有意义,就是好好活,好好活就是做很多有意义的事)

    fail 函数在 monad 的数学定义中并不是一个必须的部分,但它由于它在Haskell do形式中扮演的角色,所以他包含在标准Monad类定义中。在 do 语句块中,一旦模式匹配失败就会调用fail函数。

    fn :: Int -> Maybe [Int] 
    fn idx = do let l = [Just [1,2,3], Nothing, Just [], Just [7..20]] 
                (x:xs) <- l!!idx   -- a pattern match failure will call "fail" 
                return xs 
    

    在上面的代码中, fn 0 返回 Just [2, 3], 但是fn 1 和 fn 2都返回 Nothing。

    >> 函数是一个简便函数,用于计算序列中,bind一个不需要前面计算结果的monadic计算。用>>=来定义:

    (>>) :: m a -> m b -> m b 
    m >> k = m >>= (\_ -> k) 
    

    No way out

    (被T1老大鞭策之,不能惰遁)

    你可能已经注意到了,没有办法把在标准的Monad类中的值取出Monad。这并不意外。并没有什么阻止monad的作者去用针对monad的函数。比如,可以用模式匹配 Just x 或 fromJust函数来吧 Maybe monad 中的值抽取出来。

    通过不提供这样的函数,Haskell Monad类允许创建“单向”Monad。单向monad允许通过 return (有时是 fail)把值放入 monad,并且允许在monad中用 >>= 和 >>执行计算,但是不允许把值传出Monad。

    IO monad是 Haskell中一个常见的单向monad的例子。因为你无法从IO monad中逃离,不可能写一个在 IO monad中进行了计算但结果的类型里不包括IO类型构造子的函数。这意味着任何结果不带有IO类型构造子的函数都保证不使用 IO monad。其他monad,如List,Maybe允许把值传出monad。因此可以写内部使用了这些monad但返回非 monadic 值的函数。

    关于“单向”monad的一个非常棒的特性是:在它的monadic操作中支持“副作用”且阻止这些副作用破坏程序中的非monadic部分的函数式特性。(译注:IO monad保持了 Haskell纯函数式语言的一致性,把副作用隔离到一个可控的范围内.)

    考虑一个从用户读取一个字符的简单情况。我们不能简单地写一个函数 readChar :: Char,因为依赖于用户的输入,每次调用都需要返回不同的字符。Haskell作为纯函数式语言的一个基本属性就是所有的函数用同样的参数调用两次返回同样的值。但在 IO monad 中用 getChar :: IO Char 来做 I/O 操作却没问题,因为它只能在一个单向的monad中串行的使用。一个函数一旦使用了带IO类型的函数,便无法在其函数类型中去掉 IO 类型,这样IO类型构造子便可以标示出所有执行了 I/O 操作的函数。进一步说,这些函数也只能在一个 IO monad中有用。这样,一个单向的monad有效的创建了一个隔离环境,在这个环境中纯函数式语言的规则可以不那么严格。函数式的计算可以放进这个环境,但是危险的副作用和违反引用透明性的函数则无法逃离这个环境。

    定义monad的另一种通常模式是用函数来表示monadic的值。这样当需要一个monadic的计算的值时,返回的monad是一次“执行”来给出结果。

    Zero and Plus

    除了上面说的monad三个规则之外,一些monad还遵守附加规则。有一个特殊值 mzero 和一个操作符 mplus 的monad还需遵守四个附加的规则:

    	mzero >>= f == mzero 
    	m >>= (\x -> mzero) == mzero 
    	mzero `mplus` m == m 
    	m `mplus` mzero == m 
    

    把mzero, mplus, >>= 与普通数学中的 0, +, * 联系起来就好记了。

    有zero和plus的monad可以用 Haskell中的 MonadPlus类来声明:

    class (Monad m) => MonadPlus m where 
        mzero :: m a 
        mplus :: m a -> m a -> m a 
    

    继续用 Maybe monad作例子,Maybe是一个MonadPlus的 instance:

    instance MonadPlus Maybe where 
        mzero             = Nothing 
        Nothing `mplus` x = x 
        x `mplus` _       = x 
    

    这确定了 Nothing 作为 zero, 把两个 Maybe的值放在一起相加则返回其中第一个不为 Nothing的值.当两个输入值都是Nothing的时候, mplus 的结果也是 Nothing.

    List monad 也有zero 和 plus, mzero就是空表, mplus就是 ++ 操作符.

    mplus操作符用来把分离的计算组合成一个单独的monadic的值。在我们的克隆羊的例子中,我们可以用Maybe的 mplus来定义一个函数, parent s = (mother s) `mplus` (fater s),如果s有一个父亲或母亲的话则返回一个,如果根本没有父母的话则返回Nothing。如果一只羊父母都有,那么返回父亲或母亲要看Maybe monad中mplus的具体定义。

    Summary

    Monad class的Instance 需要满足所谓的“monad规则”,这些规则描述了monad对代数特性。有三个规则规定了“return”函数是一个左单位元,并且是一个右单位元,“bind”操作具有结合性。如不满足这三个规则,在用do形式时函数行为将不正常并产生微妙的问题。

    除了 return 和 >>= 函数,Monad class定义了另外的函数 fail。 fail 函数并不是monad所必需的,但在实际中它经常很有用,而且haskell的 do 形式用到了它,因此它被包含着Monad Class中。

    有些monad遵守三个基本规则之外的规则。这样的monad中有一类很重要,他们包含 zero元素和 plus操作符。Haskell为这类monad提供了 MonadPlus class,里面定义了 mzero值和mplus操作。