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開始介紹吧:
1 trait Monad[F[_]] extends Applicative[F] with Bind[F] { self =>
2 //// scalaz/Monad.scala
3
4 override def map[A,B](fa: F[A])(f: A => B) = bind(fa)(a => point(f(a))) 5 ... 6 trait Applicative[F[_]] extends Apply[F] { self =>
7 //// scalaz/Applicative.scala
8 def point[A](a: => A): F[A] 9 ... 10 trait Apply[F[_]] extends Functor[F] { self =>
11 //// scalaz/Apply.scala
12 def ap[A,B](fa: => F[A])(f: => F[A => B]): F[B] 13 ... 14 trait Bind[F[_]] extends Apply[F] { self =>
15 //// scalaz/Bind.scala
16
17 /** Equivalent to `join(map(fa)(f))`. */
18 def bind[A, B](fa: F[A])(f: A => F[B]): F[B] 19
20 override def ap[A, B](fa: => F[A])(f: => F[A => B]): F[B] = { 21 lazy val fa0 = fa 22 bind(f)(map(fa0)) 23 } 24 ...
上面這些類型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
1 final class BindOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Bind[F]) extends Ops[F[A]] { 2 ////
3 import Liskov.<~<, Leibniz.===
4
5 def flatMap[B](f: A => F[B]) = F.bind(self)(f) 6
7 def >>=[B](f: A => F[B]) = F.bind(self)(f) 8
9 def ∗[B](f: A => F[B]) = F.bind(self)(f) 10 ...
主要是這個flatMap函數,在scalaz里用>>=來表示。這是一個大家都起碼耳熟的函數:好像flatMap就代表了Monad。在MonadOps里提供的注入方法如下:scalaz/Syntax/MonadSyntax.scala
1 final class MonadOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Monad[F]) extends Ops[F[A]] { 2 ////
3
4 def liftM[G[_[_], _]](implicit G: MonadTrans[G]): G[F, A] = G.liftM(self) 5
6 def whileM[G[_]](p: F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.whileM(p, self) 7
8 def whileM_(p: F[Boolean]): F[Unit] = F.whileM_(p, self) 9
10 def untilM[G[_]](p: => F[Boolean])(implicit G: MonadPlus[G]): F[G[A]] = F.untilM(self, p) 11
12 def untilM_(p: => F[Boolean]): F[Unit] = F.untilM_(self, p) 13
14 def iterateWhile(p: A => Boolean): F[A] = F.iterateWhile(self)(p) 15
16 def iterateUntil(p: A => Boolean): F[A] = F.iterateUntil(self)(p) 17
18 ////
19 }
看起來這些注入方法都是一些編程語言里的流程控制語法(control flow syntax)。這是不是暗示着Monad最終會實現某種編程語言?我們把這些函數的使用方法放在后面的一些討論去。我們先來分析一下flatMap函數,因為這是個Monad代表函數。下面是Functor,Applicative和Monad施用函數格式比較:
1 // Functor : map[A,B] (F[A])(f: A => B): F[B] 2 // Applicative: ap[A,B] (F[A])(f: F[A => B]): F[B] 3 // 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串聯起來的話就會是這樣的:
1 // fa.flatMap(a => fb.flatMap(b => fc.flatMap(c => fd.map(...))))
1 // for { 2 // a <- (fa: F[A]) 3 // b <- (fb: F[A]) 4 // c <- (fc: F[A]) 5 // } yield { ... }
1 class Foo { def bar: Option[Bar] } 2 class Bar { def baz: Option[Baz] } 3 class Bar { def baz: Option[Baz] } 4
5 def compute(maybeFoo: Option[Foo]): Option[Int] =
6 maybeFoo.flatMap { foo =>
7 foo.bar.flatMap { bar =>
8 bar.baz.map { baz =>
9 baz.compute 10 } 11 } 12 } 13 def compute2(maybeFoo: Option[Foo]): Option[Int] =
14 for { 15 foo <- maybeFoo 16 bar <- foo.bar 17 baz <- bar.baz 18 } yield baz.compute
1 // ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a)))
1 // map[A,B](fa: F[A])(f: A => B): F[B] = fa.flatMap(a => point(f(a)))
從上面的例子好像可以領悟一些關於FP即Monadic Programming的說法。形象的來講:這個所謂的算法Monad F[]就好像是在F[]這么個殼子里進行傳統編程:還記着的話,FP編程既是純函數(pure function)對F[T]里的T值進行運算,沒有中間變量(temp variable),沒有副作用(no side-effect)。但現在有了Monad,我們就可以使用傳統的行令編程(imperative programming)了。再形象一點來說上面的for loop就像F[]殼子,在for loop內可以進行申明變量,更新狀態等OOP式行令編程。但這些變化(mutability)不會漏出for loop之外。不過,本篇所述Monad編程的單一局限性還是很明顯的:因為在for loop 內部的操作函數都必須返回同一種類型的Monad實例如:Option[], List[],SomeType[]等等。而且程序運算行為只會受一種類型的特性所控制。如上面所敘,Monad實例的類型控制Monadic程序的運算行為。每一種Monad實例的程序可以有不同的運算方式。如果需要多種類型行為的Monad程序,就需要使用Monad Transformer typeclass了。這個在將來的討論中自會提及,現在好像說的過頭了。我們還是回到Monad的基本操作。
Option是scala標准庫的一個類型。它已經是個Monad,所以可以使用flatMap:
1 2.some flatMap {x => (x + 3).some } //> res0: Option[Int] = Some(5)
2 2.some >>= { x => (x + 3).some } //> res1: Option[Int] = Some(5)
3 (none: Option[Int]) >>= {x => (x + 3).some } //> res2: Option[Int] = None
我們可以用Monad[T] point來把一個普通值A升格到T[A]:
1 Monad[Option].point(2) //> res3: Option[Int] = Some(2)
2 Monad[Option].point(2) >>= {x => Monad[Option].point(x + 3)} 3 //> res4: Option[Int] = Some(5)
4 (None: Option[Int]) >>= {x => Monad[Option].point(x + 3)} 5 //> res5: Option[Int] = None
在上面的例子里我們不斷提及Option Monad是有原因的,因為Option類型的Monad典型實例,在控制運算流程時最有特點:可以在中途退出,在遇到None值時可以立即終止運算。
我們用一個比較現實點的例子來示范:我正嘗試用自己的方式來練習舉重 - 我最多能舉起50KG、每個杠鈴片重2.5公斤、杠鈴兩端不必平衡,但一邊不得超過另一邊多於3個杠鈴片(多3個還沒問題)。試着用一個自定義類型來模擬舉重:
1 type Discs = Int //杠鈴片數量
2 case class Barbell(left: Discs, right: Discs) { 3 def loadLeft(n: Discs): Barbell = copy(left = left + n) 4 def loadRight(n: Discs): Barbell = copy(right = right + n) 5 } 6 Barbell(0,0).loadLeft(1) //> res8: Exercises.monad.Barbell = Barbell(1,0)
7 Barbell(1,0).loadRight(1) //> res9: Exercises.monad.Barbell = Barbell(1,1)
8 Barbell(2,1).loadLeft(-1) //> res10: Exercises.monad.Barbell = Barbell(1,1)
現在這個自定義類型Barbell是可以跟蹤當前杠鈴左右重量狀態的。現在我把往杠鈴上增加重量片的過程串聯起來:
1 Barbell(0,0).loadLeft(1).loadRight(2).loadRight(100).loadLeft(2).loadRight(-99) 2 //> res11: Exercises.monad.Barbell = Barbell(3,3)
可以看到這個過程中有些環節已經超出了我的能力,但杠鈴最終狀態好像還是合理的。我們需要在重量配置不合理的時候就立即終止。現在我們可以用Option來實現這項功能:
1 type Discs = Int //杠鈴片數量
2 case class Barbell(left: Discs, right: Discs) { 3 def loadLeft(n: Discs): Option[Barbell] = copy(left = left + n) match { 4 case Barbell(left,right) => if ( (left+right <= 20) && math.abs(left-right) <=3 ) Some(Barbell(left,right)) else None 5 case _ => None 6 } 7 def loadRight(n: Discs): Option[Barbell] = copy(right = right + n) match { 8 case Barbell(left,right) => if ( (left+right <= 20) && math.abs(left-right) <=3 ) Some(Barbell(left,right)) else None 9 case _ => None 10 } 11 } 12 Barbell(0,0).loadLeft(1) //> res8: Option[Exercises.monad.Barbell] = Some(Barbell(1,0))
13 Barbell(1,0).loadRight(1) //> res9: Option[Exercises.monad.Barbell] = Some(Barbell(1,1))
14 Barbell(2,1).loadLeft(-1) //> res10: Option[Exercises.monad.Barbell] = Some(Barbell(1,1))
15 Barbell(0,0).loadLeft(4) //> res11: Option[Exercises.monad.Barbell] = None
16 Barbell(15,1).loadRight(15) //> res12: Option[Exercises.monad.Barbell] = None
超出重量平衡的情況返回了None。現在返回值是個Option,而Option是個Monad,所以我們可以用flatMap把每個環節串聯起來:
1 Barbell(0,0).loadLeft(3) >>= {_.loadRight(3)} //> res13: Option[Exercises.monad.Barbell] = Some(Barbell(3,3))
2 Barbell(0,0).loadLeft(3) >>= {_.loadRight(3) >>= {_.loadRight(1)}} 3 //> res14: Option[Exercises.monad.Barbell] = Some(Barbell(3,4))
4 Barbell(0,0).loadLeft(3) >>= {_.loadRight(3) >>= {_.loadRight(1) >>= {_.loadLeft(4)}}} 5 //> res15: Option[Exercises.monad.Barbell] = Some(Barbell(7,4))
6 Barbell(0,0).loadLeft(1) >>= {_.loadRight(5) >>= {_.loadLeft(2)}} 7 //> res16: Option[Exercises.monad.Barbell] = None
8 Monad[Option].point(Barbell(0,0)) >>= {_.loadLeft(3) >>= {_.loadRight(6)}} 9 //> res17: Option[Exercises.monad.Barbell] = Some(Barbell(3,6))
我們的最終目的是用for-comprehension來表述,會更加清晰:
1 def addWeight: Option[Barbell] = for { 2 b0 <- Monad[Option].point(Barbell(0,0)) 3 b1 <- b0.loadLeft(3) 4 b2 <- b1.loadRight(3) 5 } yield b2 //> addWeight: => Option[Exercises.monad.Barbell]
6 addWeight //> res18: Option[Exercises.monad.Barbell] = Some(Barbell(3,3))
7
8 def addWeight1: Option[Barbell] = for { 9 b0 <- Monad[Option].point(Barbell(0,0)) 10 b1 <- b0.loadLeft(4) 11 b2 <- b1.loadRight(3) 12 } yield b2 //> addWeight1: => Option[Exercises.monad.Barbell]
13 addWeight1 //> res19: Option[Exercises.monad.Barbell] = None
從以上的例子可以得出:實現了一個數據類型的Monad實例后就可以獲取以這個類型控制運算行為的一種簡單的編程語言,這種編程語言可以在for loop內部實現傳統的行令編程風格。
在本篇討論中我們介紹了Monad實際上是一種編程模式,並且示范了簡單的for loop內部流程運算。在下面的一系列討論中我們將會了解更多類型的Monad,以及Monad如何能成為功能完善的編程語言。
