> F[D])...))。用flatMap链表示:
1 fa.flatMap(a => fb.flatMap(b => fc.flatMap(c => fd.map(...))))
从flatMap串联就比较容易观察到Monad运算的关联依赖性和串联行:后面一个运算需要前面那个运算的结果。而在Option Monad里如果前面的运算产生结果是None的话,串联运算终止并直接返回None作为整串运算的结果。
值得提醒的是连串的flatMap其实也是一种递归算法,但又不属于尾递归,所以拥有和其它FP算法一样的通病:会消耗堆栈,超长的flatMap链条很容易造成堆栈溢出错误(stack overflow)。所以,直接使用Monad编程是不安全的,必须与Trampling数据结构配合使用才行。正确安全的Monad使用方式是通过Trampling结构存放原本在堆栈上的函数调用参数,以heap替换stack来防止stack-overflow。我们会在将来详细讨论Trampling原理机制。
我们可以从上面的flatMap串中推导出for-comprehension:
1 // for { 2 // a <- (fa: F[A]) 3 // b <- (fb: F[A]) 4 // c <- (fc: F[A]) 5 // } yield { ... }
从for-comprehension能够更容易看出:我们可以选择在for loop内按要求连续运算F[T]。只要我们能提供a,b,c ...作为运算元素。
按理来说除了Option Monad,其它类型的Monad都具备这种连续运算的可选择性。而Option Monad的特点就在于在运算结果为None时可以立即终止运算。
现在我们可以试着自定义一个类型然后获取个什么实例。不过我们还是要谨记自定义类型的目的何在。我看多数可能是实现Monad实例,这样我们就可以在自定义类型的控制下进行Monadic编程了,即在for-comprehension内进行熟悉的行令编程(imperative programming)。我们应该没什么需要去获取Functor或Applicative实例,而且Monad trait也继承了Functor及Applicative trait,因为map和ap都可以用flatMap来实现:
1 ef map[A,B](fa: F[A])(f: A => B): F[B] =
2 fa flatMap {a => point(f(a))} 3 def ap[A,B](fa: F[A])(ff: F[A => B]): F[B] =
4 ff flatMap { f => fa flatMap {a => point(f(a)) }}
值得注意的是:flatMap有着很明显的串性,适合于运算流程管理(workflow)。但实现并行运算就会困难了。这就是Applicative存在的主要原因。如果自定义Monad需要进行并行运算的话就要避免用flatMap实现ap。正确的方式是不用其它的组件函数,直接单独实现ap函数。
很多人自定义Monad可能就是简单希望能用for-comprehension。它是一种简单的FP编程语言(Monadic language):能在一个自定义类型的壳内(context)进行行令编程来实现程序状态转变。如上面强调的那样,我们必须先要搞清楚自定义Monad类型的目的:一开始我们希望能用FP方式实现一些简单的行令编程,如下:
1 var a = 3
2 var b = 4
3 var c = a + b
就是这么简单。不过我们希望用FP方式来实现。那么可不可以这么描述需求:对同样某一种种数据类型的变量进行赋值,然后对这些变量实施操作,在这里是相加操作。那么我们需要一个高阶类型F[T],用F来包嵌一种类型数据T。在壳内运算T后结果还是一个T类型值。
我们先定义一下这个类型吧:
1 trait Bag[A] { 2 def content: A 3 } 4 object Bag { 5 def apply[A](a: A) = new Bag[A] { def content = a } 6 }
形象点解释:一个袋子Bag里装一种可以是任何类型A的东西。
用scalaz来实现Bag类型的Monad实例很简单:
1 rait Bag[A] { 2 def content: A 3 } 4 object Bag { 5 def apply[A](a: A) = new Bag[A] { def content = a } 6 implicit object bagMonad extends Monad[Bag] { 7 def point[A](a: => A) = Bag(a) 8 def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = f(ba.content) 9 } 10 }
只要定义了point,bind函数即可。point能把一个普通类型A的值套入壳子Bag。bind既是flatMap,它决定了从一个运算连接到下一个运算过程中对壳中数据进行的附加处理。可以看到以上bagMonad的bind函数没有附加任何处理,直接对目标壳内数据(ba.content)施用传入函数f。
现在Bag已经是个Monad实例了,我们可以使用所有Monad typeclass提供的函数:
1 val chainABC = Bag(3) flatMap {a => Bag(4) flatMap {b => Bag(5) flatMap {c => Bag(a+b+c) }}} 2 //> chainABC : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@c8e4bb0
3 chainABC.content //> res0: Int = 12
4
5 val bagABC = Bag(3) >>= {a => Bag(4) >>= {b => Bag(5) map {c => (a+b+c) }}} 6 //> bagABC : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@29626d54
7 bagABC.content //> res1: Int = 12
8 val bagHello = Bag("Hello") >>= {a => Bag(" John,") >>= {b => Bag("how are you?") map {c => (a+b+c) }}} 9 //> bagHello : Exercises.monad.Bag[String] = Exercises.monad$Bag$$anon$1@5a63f5 10 //| 09
11 bagHello.content //> res2: String = Hello John,how are you?
注意我们是如何把壳内变量a,b,c从前面传导到后面的加法操作里的。我们已经实现了Monad的流程式运算。
?现在我们可以使用最希望用的for-comprehension来实现上面的行令编程了:
1 val addABC: Bag[Int] = for { 2 a <- Bag(3) 3 b <- Bag(4) 4 c