前面提到了scalaz是個函數式編程(FP)工具庫。它提供了許多新的數據類型、拓展的標准類型及完整的一套typeclass來支持scala語言的函數式編程模式。我們知道:對於任何類型,我們只需要實現這個類型的typeclass實例就可以在對這個類型施用所對應typeclass提供的所有組件函數了(combinator)。突然之間我們的焦點好像都放在了如何獲取typeclass實例上了,從而忽略了考慮為什么要使用這些typeclass及使用什么樣的typeclass這些問題了。所以可能有人會問我:如何獲取Int的Monad實例。我會反問:傻B,你瘋了嗎(are you insane)?你到底想干什么?這時傻B可能忽然會醒悟還沒真正了解自己這樣問的目的。看來我們還是回到問題的源頭,從使用scalaz的基本目的開始考慮分析了。
我們就圍繞scalaz提供的我們都熟悉的typeclass Functor, Applicative, Monad來分析說明吧,因為我們在前面對它們都進行了討論介紹,為了與scalaz提供的眾多其它typeclass有所區分,我們就暫時把它們統稱為Monadic typeclass吧。首先,這幾個Monadic typeclass不是數據類型,而是代表着某些編程的模式。我們知道FP編程和OOP編程最大的區別就是FP編程的狀態不可變性(immutability)、無副作用(no-side-effect)、純函數組合能力(pure code composability),這就要求FP編程在某種殼子(context)里進行狀態轉變(transformation)。形象點表達就是F[T]。F[]就是各種獨特的殼子(context)而T就是需要運算轉變的某種類型值。FP程序的結果形象描述就好像F[T] => F[U]: 代表在F[]殼子內對T進行運算,並把結果U保存在F[]內。既然FP編程相對於OOP編程是種全新的編程方式,那么自然需要一套新的程序狀態轉變方法,也就是一套新的操作函數施用模式了。Scalaz通過Functor, Applicative, Monad提供了三種基本的函數施用方式,它們都是針對F[T]里的T值:
1 // Functor : map[T,U] (F[T])(f: T => U): F[U] 2 // Applicative: ap[T,U] (F[T])(f: F[T => U]): F[U] 3 // Monad : flatMap[T,U](F[T])(f: T => F[U]): F[U]
以上函數施用方式產生同樣的效果:F[T] => F[U],都是典型的FP編程方式。所以可以說Monadic typeclass提供了規范的FP編程框架(template),程序員可以使用這些框架進行FP編程。如果這樣解釋使用scalaz的目的,是不是更清楚一點了?
從另一個角度解釋:scalaz typeclass 代表着抽象編程概念。typeclass是通過即興多態來實現針對各種類型值的FP式計算的。回到開頭傻B的問題:Int是一種基礎類型,換句話說就是FP函數施用的目標。Monadic typeclass針對的類型是高階的F[T]類型。我們需要對在F[]的作用環境里T類型值計算方式進行概括。我們真正需要獲取的實例實際上是針對高階類型F[_]的。所以傻B問了個錯誤的問題,肯定她當時不知自己在干什么。
現在我們可以分析一下應該使用什么typeclass了。總體來說,我的理解是可以把scalaz typeclass分成種類和特質:
種類定義了FP編程的各種模式。比如Functor, Applicative, Monad都代表不同的編程方式或者說它們都具備不同的程序運算模式。特質是指不同的數據類型所定義的typeclass實例控制着程序的具體運算行為。如Option Monad可以None狀態中途終止運算、State Monad確保狀態值一直隨着程序運算。它們都因為基於不同類型的實例而表現不同的運算行為。Functor, Applicative, Monad的特質則由它們的實例中map, ap, flatMap這三個驅動函數的具體實現方式所決定。我們先看看現成的Option Functor,它的實現方式如下:
1 mplicit object optionFunctor extends Functor[Option] { 2 def map[T,U](ot: Option[T])(f: T => U): Option[U] = ot match { 3 case Some(t) => Some(f(t)) 4 case None => None 5 } 6 }
Option Functor實例驅動函數map的意思是說如果目標類型F[T]的值是個Some,那么我們就在Some殼內施用參數提供的一般函數f;如果目標值是None就不施用函數。我們再看看List Functor:
1 implicit object listFunctor extends Functor[List] { 2 def map[T,U](lt: List[T])(f: T => U): List[U] = lt match { 3 case Nil => Nil 4 case head :: tail => f(head) :: map(tail)(f) 5 } 6 }
List Functor的map函數彰顯出對一串在殼內元素逐個轉變的特性。從List操作方式就很容易理解:list.map(t => transform(t))
我們再看看Option Applicative的實例:
1 implicit object objectApplicative extends Applicative[Option] { 2 def point[T](t: T): Option[T] = Some(t) 3 def ap[T,U](ot: Option[T])(of: Option[T => U]): Option[U] = (ot, of) match { 4 case (Some(t), Some(f)) => Some(f(t)) 5 case _ => None 6 } 7 }
Option Applicative的驅動函數ap又一次凸顯了Option的特別處理方式:只有在目標值和操作函數都不為None時才施用通過殼提供的操作函數。
再看看Option Monad實例:
1 mplicit object optionMonad extends Monad[Option] { 2 def flatMap[T,U](ot: Option[T])(f: T => Option[U]): Option[U] = ot match { 3 case Some(t) => f(t) 4 case _ => None 5 } 6 }
這個flatMap函數可以告訴我們更多東西:如果我們把Option[T]視作一個運算的話,那么只要這個運算結果不為None就可以選擇連續運算,因為:f: T => Option[U],用文字描述即為給一個T值進行計算后產生另一個運算Option[U],如果再給Option[U]一個值進行計算的話就又會產生另一個運算Opton[V]... 如此持續:
F[A](a => F[B](b => F[C](c => 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 <- Bag(5) 5 } yield a+b+c //> addABC : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@10e41621
6 addABC.content //> res2: Int = 12
7
8 val concatABC: Bag[String] =
9 for { 10 a <- Bag("hello") 11 b <- Bag(" jonh,") 12 c <- Bag("how are you ?") 13 } yield ( a+b+c) //> concatABC : Exercises.monad.Bag[String] = Exercises.monad$Bag$$anon$1@353d0 14 //| 772
15 concatABC.content //> res3: String = hello jonh,how are you ?
不要看上面的程序好像很簡單,但它代表的意義卻是重大的:首先我們實現了FP方式的狀態轉變:我們雖然使用了行令編程,但最終殼Bag內部的數據content運算結果正是我們編程時所期望的。再就是我們通過flatMap串聯持續對多個變量一一進行了賦值,然后用普通的函數把這些變量進行了結合yield (a+b+c)。可以說我們初步嘗試實現了FP編程模式(在一個什么殼內進行運算)。
前面說過,for-comprehension可以是一種簡單的FP編程語言Monadic language。用它編制的程序運算行為可以受定義它的Monad實例所控制。那么我們就試着為我們的Bag Monad增加一點影響:
1 trait Bag[+A] {} 2 case class Bagged[+A](content: A) extends Bag[A] 3 case object Emptied extends Bag[Nothing]
我們稍微調整了一下Bag類型。現在Bag由兩種狀態組成:有東西的袋子Bagged和空袋子Emptied。如果希望我們的Monadic程序在遇到Emptied時能像Option Monad那樣立即終止運算並直接返回Emptied結果,我們必須在bind函數里設定這種行為:
1 trait Bag[+A] {} 2 case class Bagged[+A](content: A) extends Bag[A] 3 case object Emptied extends Bag[Nothing] 4
5 object Bag { 6 implicit object bagMonad extends Monad[Bag] { 7 def point[A](a: => A) = Bagged(a) 8 def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = ba match { 9 case Bagged(a) => f(a) 10 case _ => Emptied 11 } 12 } 13 }
在bind函數里我們用模式匹配方式判斷輸入Bag狀態:如果是有裝東西的(Bagged)那么像上面的設計一樣直接運算f獲取下一個Bag狀態,如果是空袋子Emptied的話就不做任何運算直接返回Emptied。我們現在可以測試一下上面定義的運算:
1 val chainABC = Monad[Bag].point(3) flatMap {a => Monad[Bag].point(4) flatMap {b => Monad[Bag].point(5) flatMap {c => Bagged(a+b+c) }}} 2 //> chainABC : Exercises.monad.Bag[Int] = Bagged(12) 3 val bagABC = Monad[Bag].point(3) >>= {a => Monad[Bag].point(4) >>= {b => Monad[Bag].point(5) map {c => (a+b+c) }}} 4 //> bagABC : Exercises.monad.Bag[Int] = Bagged(12) 5 val bagHello = Monad[Bag].point("Hello") >>= {a => Monad[Bag].point(" John,") >>= {b => Monad[Bag].point("how are you?") map {c => (a+b+c) }}} 6 //> bagHello : Exercises.monad.Bag[String] = Bagged(Hello John,how are you?) 7 val addABC: Bag[Int] = for { 8 a <- Monad[Bag].point(3) 9 b <- Monad[Bag].point(4) 10 c <- Monad[Bag].point(5) 11 } yield a+b+c //> addABC : Exercises.monad.Bag[Int] = Bagged(12) 12 13 val concatABC: Bag[String] = 14 for { 15 a <- Monad[Bag].point("hello") 16 b <- Monad[Bag].point(" jonh,") 17 c <- Monad[Bag].point("how are you ?") 18 } yield ( a+b+c) //> concatABC : Exercises.monad.Bag[String] = Bagged(hello jonh,how are you ?) 19 //|
我們可以看到在Bag不是Emptied時,以上這些程序運算行為與上一個版本的Monad程序沒有區別。但是如果我們增加了Emptied呢:
1 val bagABC = Monad[Bag].point(3) >>= {a => (Bagged(4): Bag[Int]) >>= {b => Monad[Bag].point(5) >>= { c => (Emptied: Bag[Int]) map {c => (a+b+c) }}}} 2 //> bagABC : Exercises.monad.Bag[Int] = Emptied
flatMap鏈條中間出現了Emptied,運算終斷,返回Emptied結果。注意下面的表達形式:
Monad[Bag].point(3)
(Bagged(3): Bag[Int])
意思都是一樣的。但Bagged(3).flatMap這樣寫是不行的,因為Bagged(3)不明確是Bag。
再看看在for-comprehension程序中加上Emptied情況:
1 val addABC: Bag[Int] = for { 2 a <- Monad[Bag].point(3) 3 x <- (Emptied: Bag[Int]) 4 b <- Monad[Bag].point(4) 5 c <- Monad[Bag].point(5) 6 } yield a+b+c //> addABC : Exercises.monad.Bag[Int] = Emptied
7
8 val concatABC: Bag[String] =
9 for { 10 a <- Monad[Bag].point("hello") 11 x <- (Emptied: Bag[Int]) 12 b <- Monad[Bag].point(" jonh,") 13 c <- Monad[Bag].point("how are you ?") 14 } yield ( a+b+c) //> concatABC : Exercises.monad.Bag[String] = Emptied
不錯,正是我們期待的運算行為。
現在我們可以用簡單的語言來描述Monad存在的意義:它提供了一套規范的模式來支持FP編程。