说道FP,我们马上会联想到Monad。我们说过Monad的代表函数flatMap可以把两个运算F[A],F[B]连续起来,这样就可以从程序的意义上形成一种串型的流程(workflow)。更直白的讲法是:任何类型只要实现了flatMap就可以用for-comprehension, for{...}yield。在这个for{...}里我们可以好像OOP一样编写程序。这个for就是一种运算模式,它规范了在for{...}里指令的行为。我们正从OOP风格走入FP编程模式,希望有个最基本的FP编程模式使我们能够沿用OOP编程风格的语法和思维。Monad应该就是最合适的泛函数据类型了。我们先从最基本的开始:假如我们有一段行令程序:
/* val a = e1 val b = e2(a) val c = e3(a,b) val d = e2(c) */
通过这些函数e1,e2,e3最后计算出d值。如果是用FP风格来编这段程序的话,首先我们必须把函数的结果d放入F[d]的F里。F就是上面所说的运算模式,在这里可以用大家熟悉的context(上下文)来表示。F必须是个Monad,F[]相当于for{...}yield。我们先试试用Id,虽然Id[A]对A不做任何处理,直接返回,好像没什么意义,但这种类型具备了map和flatMap,应该可以用for-comprehension:
1 import scalaz._ 2 import Scalaz._ 3 def e1:Id[Int] = 10 //> e1: => scalaz.Scalaz.Id[Int]
4 def e2(a: Int): Id[Int] = a + 1 //> e2: (a: Int)scalaz.Scalaz.Id[Int]
5 def e3(a: Int, b: Int): Id[Int] = a + b //> e3: (a: Int, b: Int)scalaz.Scalaz.Id[Int]
6 for { 7 a <- e1 8 b <- e2(a) 9 c <- e3(a,b) 10 d <- e2(c) 11 } yield d //> res0: scalaz.Scalaz.Id[Int] = 22
可以看到,在for-loop里就是OOP的行令程序。不过如果觉着这个Id没什么意义,可以试试Option看:
1 import scalaz._ 2 import Scalaz._ 3 def e1:Option[Int] = 10.some //> e1: => Option[Int]
4 def e2(a: Int): Option[Int] = (a + 1).some //> e2: (a: Int)Option[Int]
5 def e3(a: Int, b: Int): Option[Int] = (a + b).some//> e3: (a: Int, b: Int)Option[Int]
6 for { 7 a <- e1 8 b <- e2(a) 9 c <- e3(a,b) 10 d <- e2(c) 11 } yield d //> res0: Option[Int] = Some(22)
看,虽然换了个壳子(context), 但for-loop里的程序没有变化。换一句话讲就是for-loop里的程序根本不理会包裹的context。
Reader也是一种Monad,用它又怎样呢:
1 import scalaz._ 2 import Scalaz._ 3 def e1:Reader[Int,Int] = Reader[Int,Int](a => a) //> e1: => scalaz.Reader[Int,Int]
4 def e2(a: Int): Reader[Int,Int] = Reader[Int,Int](_ => a + 1) 5 //> e2: (a: Int)scalaz.Reader[Int,Int]
6 def e3(a: Int, b: Int): Reader[Int, Int] = Reader[Int,Int](_ => a+b) 7 //> e3: (a: Int, b: Int)scalaz.Reader[Int,Int]
8 val prg = for { 9 a <- e1 10 b <- e2(a) 11 c <- e3(a,b) 12 d <- e2(c) 13 } yield d //> prg : scalaz.Kleisli[scalaz.Id.Id,Int,Int] = Kleisli(<function1>)
14 prg.run(10) //> res0: scalaz.Id.Id[Int] = 22
虽然在语法上有些蹩脚,但还是证明了for-loop里的程序是不理会外面context的。那么我们可不可以说这个prg就是一个简单的FP编程语言。它把运算结果放在context里,直至运行了某种interpreter才能取得实际的运算值(用run(10)得到22)。当然,一段程序,它的运算行为受制于单一种类型的context可能有些弱了。如果需要获得一种可用的FP编程语言,我们可能还是要探讨如何把单一类型context组合成多类型混合的context。
我们发现在scalaz里有些type class的名称是以T结束的如:ReaderT,WriterT,StateT等等。这个T指的是变形器Transformer,意思是用它可以堆砌(stacking)context。看看StateT,简单定义应该是这样的:
case class StateT[F[_],S,A](run: S => F[(S,A)])
我们可以把F类堆砌在State上。实践证明如果这个F实现了flatMap,那么堆砌成的类型也能实现flatMap。好,scalaz的Option是实现了flatMap的,那么能不能把它和State堆砌在一起呢?堆砌而成的context会有什么效果呢?我们先看看单一Option和State作为一种context的效果:
1 for { 2 a <- 3.some 3 b <- (None: Option[Int]) 4 c <- 4.some 5 } yield c //> res1: Option[Int] = None
6 val statePrg = for { 7 a <- get[Int] 8 b <- State[Int,Int](s => (s, s + a)) 9 _ <- put(9) 10 } yield b //> statePrg : scalaz.IndexedStateT[scalaz.Id.Id,Int,Int,Int] = scalaz.IndexedS 11 //| tateT$$anon$10@15ff3e9e
12 statePrg.run(3) //> res2: scalaz.Id.Id[(Int, Int)] = (9,6)
依我来看,Option主要效果是在遇到None值时立即退出。而State的主要作用是在运算同时可以维护一个状态。那么如果把Option和State叠加起来就会同时具备这两种类型的特点了吧?也就是既能维护状态又能在遇到None值时立即终止运算退出了。首先验证一下用Option的flatMap来实现叠加context的flatMap: