先从最简单的情况开始,我们用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=>B
与 B=>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
,它有两个子类:Some
和None
,其中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
与后续的行为结合时,会忽略后续的行为。
看monads are elephant原文没太看懂,看你的这篇看懂啦!尤其是这篇最上面的例子,还有普通函数的两个问题,有点理解了monad。灰常感谢!Endofunctor的那个HASK图是这个系列唯一没看懂的地方。另外,还是没有看懂endofunctor如何跟functor结合,因此还是没有理解这句话“一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已”。
还是非常感谢博主!scala还有很多地方要在这个blog里学到!
把monad看做行为的组合子,闭包也是接收一个外部自由变量返回另外一个行为。有可比性吗?
没可比性。
啊。。能解释下理由吗 表象上感觉是差不多的啊
完全不在一个层面。