Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming。这其中透露的Monad重要性则不言而喻。Scalaz是通过Monad typeclass为数据运算的程序提供了一套规范的编程方式,如常见的for-comprehension。而不同类型的Monad实例则会支持不同的程序运算行为,如:Option Monad在运算中如果遇到None值则会中途退出;State Monad会确保状态值会伴随着程序运行流程直到终结;List Monad运算可能会产生多个结果等等。Scalaz提供了很多不同种类的Monad如:StateMonad, IOMonad, ReaderMonad, WriterMonad,MonadTransformer等等,这从另一个角度也重申了Monad概念在泛函编程里的重要性。听起来以上这些描述好像有点摸不着头脑,可能应该把它们放在本篇最终总结,不过我还是想让大家有个大的概念。对下面的讨论细节的理解能有所帮助。我们还是从Monad trait开始介绍吧:
trait Monad[F[_]] extends Applicative[F] with Bind[F] { self => //// scalaz/Monad.scala override def map[A,B](fa: F[A])(f: A => B) = bind(fa)(a => point(f(a))) ... trait Applicative[F[_]] extends Apply[F] { self => //// scalaz/Applicative.scala def point[A](a: => A): F[A] ... trait Apply[F[_]] extends Functor[F] { self => //// scalaz/Apply.scala def ap[A,B](fa: => F[A])(f: => F[A => B]): F[B] ... trait Bind[F[_]] extends Apply[F] { self => //// scalaz/Bind.scala /** Equivalent to `join(map(fa)(f))`. */ def bind[A, B](fa: F[A])(f: A => F[B]): F[B] override def ap[A, B](fa: => F[A])(f: => F[A => B]): F[B] = { lazy val fa0 = fa bind(f)(map(fa0)) } ...
上面这些类型trait的继承关系是这样的:Monad继承了Applicative和Bind,Applicative继承了Apply, Apply继承了Functor, Bind也继承了Apply。所以Monad同时又是Applicative和Functor,因为Monad实现了map和ap函数。一个Monad实例可以调用所有Applicative和Functor提供的组件函数。任何实例只需要实现抽象函数point和bind就可以成为Monad实例,然后就可以使用Monad所有的组件函数了。
Monad所提供的主要注入方法(injected method)是在BindOps和MonadOps里。在BindOps里主要提供了flatMap: scalaz/syntax/BindSyntax.scala
final class BindOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Bind[F]) extends Ops[F[A]] { //// import Liskov.<~<, Leibniz.=== def flatMap[B](f: A => F[B]) = F.bind(self)(f) def >>=[B](f: A => F[B]) = F.bind(self)(f) def ∗[B](f: A => F[B]) = F.bind(self)(f) ...
主要是这个flatMap函数,在scalaz里用>>=来表示。这是一个大家都起码耳熟的函数:好像flatMap就代表了Monad。在MonadOps里提供的注入方法如下:scalaz/Syntax/MonadSyntax.scala
final class MonadOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Monad[F]) extends Ops[F[A]] { //// def liftM[G[_[_], _]](implicit G: MonadTrans[G]): G[F, A] = G.liftM(self) def whileM[G[_]](p: F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.whileM(p, self) def whileM_(p: F[Boolean]): F[Unit] = F.whileM_(p, self) def untilM[G[_]](p: => F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.untilM(self, p) def untilM_(p: => F[Boolean]): F[Unit] = F.untilM_(self, p) def iterateWhile(p: A => Boolean): F[A] = F.iterateWhile(self)(p) def iterateUntil(p: A => Boolean): F[A] = F.iterateUntil(self)(p) //// }
看起来这些注入方法都是一些编程语言里的流程控制语法(control flow syntax)。这是不是暗示着Monad最终会实现某种编程语言?我们把这些函数的使用方法放在后面的一些讨论去。我们先来分析一下flatMap函数,因为这是个Monad代表函数。下面是Functor,Applicative和Monad施用函数格式比较:
// Functor : map[A,B] (F[A])(f: A => B): F[B] // Applicative: ap[A,B] (F[A])(f: F[A => B]): F[B] // Monad : flatMap[A,B](F[A])(f: A => F[B]): F[B]
以上三种函数款式基本上是一致的。大家都说这就是三种FP的函数施用方式:在一个容器内进行函数的运算后把结果还留在容器内、得到的效果是这样的:F[A] => F[B]。只是它们分别用不同的方式提供这个施用的函数。Functor的map提供了普通函数,Applicative通过容器提供了施用函数ap而Monad则是通过直接函数施用方式来实现F[A] => F[B]: 直接对输入A进行函数施用并产生一个F[B]结果。Monad的这种方式应该不是严格意义上的在容器内进行函数施用。从另一个角度分析,Monad可以被视作某种算法(computation)。Monad F[A]代表了对一个A类型数据的算法(computation)。如果这样说那么Monad就有了全新的解释:Monad就是一种可以对某种类型的数据值进行连续计算的算法(computation):如果我们把flatMap串联起来的话就会是这样的:
// fa.flatMap(a => fb.flatMap(b => fc.flatMap(c => fd.map(...))))
// for { // a <- (fa: F[A]) // b <- (fb: F[A]) // c <- (fc: F[A]) // } yield { ... }
class Foo { def bar: Option[Bar] } class Bar { def baz: Option[Baz] } class Bar { def baz: Option[Baz] } def compute(maybeFoo: Option[Foo]): Option[Int] = maybeFoo.flatMap { foo => foo.bar.flatMap { bar => bar.baz.map { baz => baz.compute } } } def compute2(maybeFoo: Option[Foo]): Option[Int] = for { foo <- maybeFoo bar <- foo.bar baz <- bar.baz } yield baz.compute
// ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a)))
// map[A,B](fa: F[A])(f: A => B): F[B] = fa.flatMap(a => point(f(a)))
Option是scala标准库的一个类型。它已经是个Monad,所以可以使用flatMap:
2.some flatMap {x => (x + 3).some } //> res0: Option[Int] = Some(5) 2.some >>= { x => (x + 3).some } //> res1: Option[Int] = Some(5) (none: Option[Int]) >>= {x => (x + 3).some } //> res2: Option[Int] = None
Monad[Option].point(2) //> res3: Option[Int] = Some(2) Monad[Option].point(2) >>= {x => Monad[Option].point(x + 3)} //> res4: Option[Int] = Some(5) (None: Option[Int]) >>= {x => Monad[Option].point(x + 3)} //> res5: Option[Int] = None
我们用一个比较现实点的例子来示范:我正尝试用自己的方式来练习举重 - 我最多能举起50KG、每个杠铃片重2.5公斤、杠铃两端不必平衡,但一边不得超过另一边多于3个杠铃片(多3个还没问题)。试着用一个自定义类型来模拟举重:
type Discs = Int //杠铃片数量 case class Barbell(left: Discs, right: Discs) { def loadLeft(n: Discs): Barbell = copy(left = left + n) def loadRight(n: Discs): Barbell = copy(right = right + n) } Barbell(0,0).loadLeft(1) //> res8: Exercises.monad.Barbell = Barbell(1,0) Barbell(1,0).loadRight(1) //> res9: Exercises.monad.Barbell = Barbell(1,1) Barbell(2,1).loadLeft(-1) //> res10: Exercises.monad.Barbell = Barbell(1,1)
Barbell(0,0).loadLeft(1).loadRight(2).loadRight(100).loadLeft(2).loadRight(-99) //> res11: Exercises.monad.Barbell = Barbell(3,3)
可以看到这个过程中有些环节已经超出了我的能力,但杠铃最终状态好像还是合理的。我们需要在重量配置不合理的时候就立即终止。现在我们可以用Option来实现这项功能:
type Discs = Int //杠铃片数量 case class Barbell(left: Discs, right: Discs) { def loadLeft(n: Discs): Option[Barbell] = copy(left = left + n) match { case Barbell(left,right) => if ( (left+right <= 20) && math.abs(left-right) <=3 ) Some(Barbell(left,right)) else None case _ => None } def loadRight(n: Discs): Option[Barbell] = copy(right = right + n) match { case Barbell(left,right) => if ( (left+right <= 20) && math.abs(left-right) <=3 ) Some(Barbell(left,right)) else None case _ => None } } Barbell(0,0).loadLeft(1) //> res8: Option[Exercises.monad.Barbell] = Some(Barbell(1,0)) Barbell(1,0).loadRight(1) //> res9: Option[Exercises.monad.Barbell] = Some(Barbell(1,1)) Barbell(2,1).loadLeft(-1) //> res10: Option[Exercises.monad.Barbell] = Some(Barbell(1,1)) Barbell(0,0).loadLeft(4) //> res11: Option[Exercises.monad.Barbell] = None Barbell(15,1).loadRight(15) //> res12: Option[Exercises.monad.Barbell] = None
超出重量平衡的情况返回了None。现在返回值是个Option,而Option是个Monad,所以我们可以用flatMap把每个环节串联起来:
Barbell(0,0).loadLeft(3) >>= {_.loadRight(3)} //> res13: Option[Exercises.monad.Barbell] = Some(Barbell(3,3)) Barbell(0,0).loadLeft(3) >>= {_.loadRight(3) >>= {_.loadRight(1)}} //> res14: Option[Exercises.monad.Barbell] = Some(Barbell(3,4)) Barbell(0,0).loadLeft(3) >>= {_.loadRight(3) >>= {_.loadRight(1) >>= {_.loadLeft(4)}}} //> res15: Option[Exercises.monad.Barbell] = Some(Barbell(7,4)) Barbell(0,0).loadLeft(1) >>= {_.loadRight(5) >>= {_.loadLeft(2)}} //> res16: Option[Exercises.monad.Barbell] = None Monad[Option].point(Barbell(0,0)) >>= {_.loadLeft(3) >>= {_.loadRight(6)}} //> res17: Option[Exercises.monad.Barbell] = Some(Barbell(3,6))
我们的最终目的是用for-comprehension来表述,会更加清晰:
def addWeight: Option[Barbell] = for { b0 <- Monad[Option].point(Barbell(0,0)) b1 <- b0.loadLeft(3) b2 <- b1.loadRight(3) } yield b2 //> addWeight: => Option[Exercises.monad.Barbell] addWeight //> res18: Option[Exercises.monad.Barbell] = Some(Barbell(3,3)) def addWeight1: Option[Barbell] = for { b0 <- Monad[Option].point(Barbell(0,0)) b1 <- b0.loadLeft(4) b2 <- b1.loadRight(3) } yield b2 //> addWeight1: => Option[Exercises.monad.Barbell] addWeight1 //> res19: Option[Exercises.monad.Barbell] = None
从以上的例子可以得出:实现了一个数据类型的Monad实例后就可以获取以这个类型控制运算行为的一种简单的编程语言,这种编程语言可以在for loop内部实现传统的行令编程风格。
在本篇讨论中我们介绍了Monad实际上是一种编程模式,并且示范了简单的for loop内部流程运算。在下面的一系列讨论中我们将会了解更多类型的Monad,以及Monad如何能成为功能完善的编程语言。