我所理解的monad(7):把monad看做行为的组合子

先从最简单的情况开始,我们用monad封装一段执行逻辑,然后提供一个map组合子,可以组合后续行为:

// 一个不完善的monad
class M[A](inner: => A) {

    // 执行逻辑
    def apply() = inner

    // 组合当前行为与后续行为,返回一个新的monad
    def map[B](next: A=>B): M[B] = new M(next(inner))
}

用上面的例子模拟几个行为的组合:

scala> val first = new M({println("first"); "111"})
first: M[String] = M@745b171c

scala> val second = first.map(x => {println("second"); x.toInt})
second: M[Int] = M@155f28dc

scala> val third = second.map(x => {println("third"); x + 200})
third: M[Int] = M@b345419

scala> third()
first
second
third
res0: Int = 311

看上去对行为的组合也不过如此,其实在Function1这个类里已经提供了对只有一个参数的函数的组合:

scala> class A; class B; class C

scala> val f1: A=>B = (a:A) => new B

scala> val f2: B=>C = (b:B) => new C

scala> val f3 = f1 andThen f2
f3: A => C = <function1>

Function1里的andThen方法可以把前面函数的结果交给后边的函数处理,A=>BB=>C 组成函数 A=>C

这样看上去我们当前实现的monad和Function1有些相似,封装了一段行为,提供方法将下一个行为组合成新的行为(可以不断的组合下去),在java里我们可以对比Command模式和Composite模式。

真实世界的”行为”(Action),普通函数的问题

1) 结果的不确定性,行为链的校验问题

这个不完善的monad已经可以做一些事情,但有个很显著的问题,只能组合”普通函数(plain)”,所谓普通函数是指结果类型就是我们想要的数据类型。

比如f: A=>B 这个函数可以表示一个行为,这个行为最终得到的数据类型是B,但如果这个行为遇到异常,或者返回为null,这时我们的组合过程会如何?

这意味着我们必须在组合过程中判断每个函数的结果是否合法,只有合法的情况下,才能与下一步组合。你可能会想把这些判断放在组合逻辑里不就得了吗?

def map[B](next: A=>B): M[B] = { 
        val result = inner  // 执行了当前行为
        if(result != null) {
            new M(next(result))
        }else {
            null.asInstanceOf[M[B]]
        }
    }

上面的情况不是我们希望的,因为要判断当前行为(inner)的结果是否符合传递给下一个行为,只有执行当前行为才能拿到结果。而我们希望组合子做的是先把所有行为组合起来,最后再一并执行。

看来只能要求next函数在实现的时候做了异常检测,但即使next函数做了判断,也不一定完美;因为行为已经先被组合好了,执行的时候,链上的每个函数仍会被调用。假设组合了多个函数,执行时中间的一个函数即使为null,仍会传递给下一个执行(下一个也必须也对参数检测),其实这个时候已经没有必要传递下去了

在scala里对这种结果不确定的情况,提供了一个专门的Option,它有两个子类:SomeNone,其中Some表示结果是一个正常值,可以通过模式匹配来获取到这个值,而None则表示为空的情况。

如果我们Option来表示我们前面的行为,可以写为:f: A=>Option[B],即结果被一个富类型给包装起来,它表示结果可能有值,也可能为空。

2) 副作用的问题

另外一个真实世界中无法用函数式完美解决的问题是IO操作,因为IO无论如何总要伴随状态的产生或变化(也就是副作用)

一段常见的举例片段:

def read(state: State) = {
    // 返回下一状态和读取到的字符串
    (state.nextState, readLine)
}

def write(state: State, str: String) = {
    //返回下一状态,和字符串处理的结果
    //这里是print返回为Unit类型,所以最终返回一个State和Unit的二元组
    (state.nextState, println(str))
}

这两个函数类型为State => (State, String)(State,String) => (State, Unit) 在入参和出参里都伴随State,是一种”不纯粹”的函数,因为每次都执行状态都不一样,即使两次的字符串是一样的,状态也是不同的。这是一种典型的“非引用透明”问题,这样的函数无法满足结合率,函数的组合性无法保障。

要实现组合特性,需要把函数转换为符合“引用透明”的特性,我们可以通过柯里化来把两个函数转化为高阶函数:

def read = 
    (state: State) => (state.nextState, readLine)

def write(str: String) = 
    (state: State) => (state.nextState, println(str))

现在这两个函数相当于只是构造了行为,要执行它们需要后续传入“状态”才行;等于把执行推迟了;同时这两个函数现在符合“引用透明”特性了。

再进一步,为了把State在构造行为的时候给隐藏起来,我们继续重构:

class IOAction[A](expression: =>A) extends Function1[State, (State,A)] { 

    def apply(s:State) = (state.next, expression) 
}

def read = new IOAction[String](readLine)

def write(str: String) = new IOAction[Unit](println(str))

现在我们可以把read看做是一个 () => IOAction[String] 的函数,write则是:String => IOAction[Unit]的函数。

用一个”富类型”表示行为的结果

我们看到现实世界的行为,除了可以用A=>B这样的plain function描述,还有A=>Option[B]A=>IOAction[B] 这种结果是一个富类型的函数来描述。我们把后两种统一描述为:

A => Result[B]

当我们要组合一个这种形式的行为时,不能再使用map,而是flatMap组合子。

实际上,我们一开始就提到过map并不是必须的方法,flatMap才是,可以把map看做只是一种特例。把行为都用统一形式A => Result[B] 来描述,对于普通的A=>B也可以转为A=>Result[B]

现在我们看几个flatMap的例子,先看一个Option的实际例子:

scala> Some({println("111"); new A}).
        flatMap(a => {println("222");Some(new B)}).
        flatMap(b => {println("333"); None}).
        flatMap(c => {println("444"); Some(new D)})
111
222
333
res14: Option[D] = None

上面的例子看到,在组合第三步的时候,得到None,后边的第四步c => {println("444"); Some(new D)}没有被执行。这是因为None与后续的行为结合时,会忽略后续的行为。

我所理解的monad(7):把monad看做行为的组合子》上有5条评论

  1. Liang

    看monads are elephant原文没太看懂,看你的这篇看懂啦!尤其是这篇最上面的例子,还有普通函数的两个问题,有点理解了monad。灰常感谢!Endofunctor的那个HASK图是这个系列唯一没看懂的地方。另外,还是没有看懂endofunctor如何跟functor结合,因此还是没有理解这句话“一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已”。

    还是非常感谢博主!scala还有很多地方要在这个blog里学到!

    回复
  2. 刘勇

    把monad看做行为的组合子,闭包也是接收一个外部自由变量返回另外一个行为。有可比性吗?

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注