中間插播了幾篇scalaz數據類型,現在又要回到Monad專題。因為FP的特征就是Monad式編程(Monadic programming),所以必須充分理解認識Monad、熟練掌握Monad運用。曾經看到一段對Monad的描述:“Monadic for-comprehension就是一種嵌入式編....
中間插播了幾篇scalaz數據類型,現在又要回到Monad專題。因為FP的特征就是Monad式編程(Monadic programming),所以必須充分理解認識Monad、熟練掌握Monad運用。曾經看到一段對Monad的描述:“Monadic for-comprehension就是一種嵌入式編程語言,由它的Monad提供它的語法”。但如果每一種Monad的for-comprehension都獨立提供一套語法的話,這種編程語言就顯得十分單調、功能簡單了。那麼既然是FP,我們應該可以通過函數組合(function composition)來把很多簡單的for-comprehension組合成一套完善的編程語言吧?比如這樣:Option[A] >>> IO[Option[A]] >>> IO[Either[String,Option[A]]。恰恰,Monad是不支持函數組合的。先瞭解一下函數組合:Functor是可以組合的,我們可以把fa和fb組合成一個更複雜的Functor fab,我們來驗證一下:
def composeFunctor[M[_],N[_]](fa: Functor[M], fb: Functor[N]): Functor[({type mn[x] = M[N[x]]})#mn] =
new Functor[({type mn[x] = M[N[x]]})#mn] {
def map[A,B](fab: M[N[A]])(f: A => B): M[N[B]] =
fa.map(fab)(n => fb.map(n)(f))
} //> composeFunctor: [M[_], N[_]](fa: scalaz.Functor[M], fb: scalaz.Functor[N])s
//| calaz.Functor[[x]M[N[x]]]
我們來解釋一下:如果M,N都是Functor,那麼M[N[A]]也是Functor,我們可以用M[N[A]].map來運算A值。看看下麵的例子:
1 val stringlen: String => Int = _.length //> stringlen : String => Int = <function1>
2 val optionInList = List("1".some,"12".some,"123".some)
3 //> optionInList : List[Option[String]] = List(Some(1), Some(12), Some(123))
4
5 val mnFunctor = composeFunctor(Functor[List],Functor[Option])
6 //> mnFunctor : scalaz.Functor[[x]List[Option[x]]] = Exercises.monadtrans$$ano
7 //| nfun$main$1$$anon$1@130d63be
8 mnFunctor.map(optionInList)(stringlen) //> res3: List[Option[Int]] = List(Some(1), Some(2), Some(3))
那麼我們需要的Monad組合應該是這樣的:M[N[A]],M,N都是Monad,如:Either[String,Option[A]],甚至是M[N[P[A]]],三層Monad。可惜,不是所有Monad都支持函數組合的,看下麵:
def composeMonad[M[_],N[_]](ma: Monad[M], mb: Monad[N]): Monad[({type mn[x] = M[N[x]]})#mn] =
new Monad[({type mn[x] = M[N[x]]})#mn] {
def point[A](a: => A) = ma.point(mb.point(a))
def bind[A,B](mab: M[N[A]])(f: A => M[N[B]]): M[N[B]] =
??? ...
}
實現M[N[A]].bind是不可能的,大家可以試試。這就堵死了函數組合這條路。難道我們就無法使用M[N[A]]這樣的for-comprehension了嗎?畢竟像Either[String,Option[A]]這樣的組合是很普遍的啊,比如說從資料庫里讀取這樣的動作,有幾種可能:取得數據、無數據None、發生錯誤。無論如何我們先試試用for-comprehension:
1 type Result[A] = String \/ Option[A]
2 val result: Result[Int] = 62.some.right //> result : Exercises.monadtxns.Result[Int] = \/-(Some(62))
3 for {
4 optionValue <- result
5 } yield {
6 for {
7 valueA <- optionValue
8 } yield valueA + 18 //> res0: scalaz.\/[String,Option[Int]] = \/-(Some(80))
9 }
從上面可以瞭解我們必須用兩層for-comprehension才能運算A值。那麼可想而知如果是M[N[P[A]]]就需要三層for-comprehension了。這就是所謂的“下階梯式演算法”(stair-stepping)。錶面上來看stair-stepping會產生複雜臃腫的代碼,喪失FP的精簡優雅風格。但想深一層,如果其中一個Monad是會產生副作用的如IO[Option[A]],那麼上面的例子就變成這樣:
1 for {
2 optionData <- IO
3 } yield {
4 for {
5 data <- optionData
6 } yield Process(data)
7 }
我們看到在第一層運算里進行了IO運算,產生了副作用。那麼以上的代碼就不再是純代碼了,無法保障函數組合。也就是說stair-stepping會產生不純代碼,違背了FP要求。之前我們曾經討論過 ReaderWriterState Monad,它是Reader,Writer,State三個Monad的組合。在它的for-comprehension里的運算結果類型是ReaderWriterState一種,所以沒有stair-stepping憂慮。但我們必須先創建一個新的類型(不是通過函數組合的新類型)。難道我們在使用不同要求的for-comprehension時都需要重新創建一個新類型嗎,這樣不就損失了FP的代碼重覆使用特點了嗎?不,scalaz提供的Monad Transformer就是一個有效的解決方案。
scalaz為很多type class提供了Monad Transformer,它們都以T尾綴命名如OptionT、EitherT、StateT...,我們可以通過Monad Transformer來靈活地組合Monad。以OptionT為例:
1 type Error[A] = \/[String, A]
2 type Result[A] = OptionT[Error, A]
3
4 val result: Result[Int] = 62.point[Result] //> result : Exercises.monadtxns.Result[Int] = OptionT(\/-(Some(62)))
5 val transformed =
6 for {
7 value <- result
8 } yield value + 18 //> transformed : scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(\/-(S
9 //| ome(80)))
現在,運算A只需要一層context了。Result就是通過Monad Transformer產生的新類型。在上面的類型構建里,OptionT就是一個Monad Transformer、Error是固定了Left類型的Either。因為Either有兩個類型參數,我們實際上也可以直接用type lambda來表示Result[A]:
type Result[A] = OptionT[({type l[x] = \/[String,x]})#l,A]
不過這樣寫不但複雜,而且會影響編譯器的類型推導(compiler type inference)。
值得註意的是,Monad Transformer 類型的構建是由內向外反向的。比如上面的例子中OptionT是個Monad Transformer,它的類型款式是OptionT[M[_],A]。OptionT實際上是用來構建M[Option[A]],在我們的例子里就是Either[Option[A]]。我們來看看一些常用Monad Transformer的類型款式:
final case class OptionT[F[_], A](run: F[Option[A]]) {
...
final case class EitherT[F[_], A, B](run: F[A \/ B]) {
...
final case class ListT[F[_], A](run: F[List[A]]){
...
trait IndexedStateT[F[_], -S1, S2, A] { self =>
/** Run and return the final value and state in the context of `F` */
def apply(initial: S1): F[(S2, A)]
可以看到,Monad Transformer 的主要作用就在構成run這個我們稱為嵌入值了。F可以是任何普通Monad。在上面的例子就變成了:
OptionT[Either,A](run: Either[Option[A]]),這個Either[Option[A]]就是我們的目標類型。而我們在操作時如在for-comprehension中運算時使用的類型則必須統一為OptionT[Either,A]。
我們如何去構建Monad Transformer類型值呢?我們可以用Applicative[MT].point或者直接用構建器方式如OptionT(...)
//point升格
Applicative[Result].point(62) //> res0: Exercises.monadtxns.Result[Int] = OptionT(\/-(Some(62)))
//簡寫版本
62.point[Result] //> res1: Exercises.monadtxns.Result[Int] = OptionT(\/-(Some(62)))
//會產生錯誤結果
None.point[Result] //> res2: Exercises.monadtxns.Result[None.type] = OptionT(\/-(Some(None)))
"Oh,shit!".left.point[Result] //> res3: Exercises.monadtxns.Result[scalaz.\/[String,Nothing]] = OptionT(\/-(So
//| me(-\/(Oh,shit!))))
//用構建器
OptionT((None: Option[Int]).point[Error]) //> res4: scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(\/-(None))
OptionT(none[Int].point[Error]) //> res5: scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(\/-(None))
OptionT("Oh,shit!".left: Error[Option[Int]]) //> res6: scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(-\/(Oh,shit!))
與重新構建另一個類型不同的是,通過Monad Transformer疊加Monad組合形成類型的操作依然使用各組成Monad的操作函數,這些函數運算結果類型任然是對應的Monad類型,所以需要一些升格函數(lifting functions)來統一類型。而重建類型則繼承了組成Monad的操作函數,它們的運算結果類型都與新建的這個類型一致。下麵我們還是用上面的這個Either+Option例子來示範。我們把Either和Option疊加後按照不同順序可以產生Either[Option[A]]或者Option[Either[A]]兩種結果類型,所以疊加順序是非常重要的,因為這兩種類型代表著截然不同的意義:Either[Option[A]]代表一個運算結果可以是成功right或者失敗left,如果運算成功則返回一個結果或空值;而Option[Either[A]]從字面上理解好像是一個運算可以返回一個成功或失敗的運算又或者返回空值,應該是沒有任何意義的一個類型。前面我們提到過用Monad Transformer疊加Monad是由內向外反方向的:獲取Either[Option[A]]就需要用OptionT[Either,A]。而且我們需要把Either和Option升格成OptionT[Either,A],看下麵的示範:
1 type Error[A] = \/[String, A]
2 type Result[A] = OptionT[Error, A]
3
4 def getString: Option[String] = "Hello ".some //> getString: => Option[String]
5 def getResult: Error[String] = "how are you!".right
6 //> getResult: => Exercises.monadtxns.Error[String]
7 val prg: Result[String] = for {
8 s1 <- OptionT.optionT(getString.point[Error])
9 s2 <- "World,".point[Result]
10 s3 <- getResult.liftM[OptionT]
11 } yield s1+s2+s3 //> prg : Exercises.monadtxns.Result[String] = OptionT(\/-(Some(Hello World,how
12 //| are you!)))
13 prg.run //> res0: Exercises.monadtxns.Error[Option[String]] = \/-(Some(Hello World,how a
14 //| re you!))
首先,我們避免了stair-stepping,直接運算s1+s2+s3。point、OptionT.optionT、liftM分別對String,Option,Either進行類型升格形成Result[String] >>> OptionT[Error,String]。升格函數源代碼如下:
trait ApplicativeIdV[A] extends Ops[A] {
def point(implicit F: Applicative[F]): F[A] = Applicative[F].point(self)
...
trait OptionTFunctions {
def optionT[M[_]] = new (({type λ[α] = M[Option[α]]})#λ ~> ({type λ[α] = OptionT[M, α]})#λ) {
def apply[A](a: M[Option[A]]) = new OptionT[M, A](a)
}
...
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)
...
再看看組合的Monad是否實現了功能疊加,如果我們加個None轉換:
1 val prg: Result[String] = for {
2 s1 <- OptionT.optionT(getString.point[Error])
3 s0 <- OptionT(none[String].point[Error])
4 s2 <- "World,".point[Result]
5 s3 <- getResult.liftM[OptionT]
6 } yield s1+s2+s3 //> prg : Exercises.monadtxns.Result[String] = OptionT(\/-(None))
7 prg.run //> res0: Exercises.monadtxns.Error[Option[String]] = \/-(None)
加個Left效果:
1 val prg: Result[String] = for {
2 s1 <- OptionT.optionT(getString.point[Error])
3 s0 <- OptionT("Catch Error!".left: Error[Option[String]])
4 s2 <- "World,".point[Result]
5 s3 <- getResult.liftM[OptionT]
6 } yield s1+s2+s3 //> prg : Exercises.monadtxns.Result[String] = OptionT(-\/(Catch Error!))
7 prg.run //> res0: Exercises.monadtxns.Error[Option[String]] = -\/(Catch Error!)
的確,用Monad Transformer組合Monad後可以實現成員Monad的效果疊加。
不過,在實際應用中兩層以上的Monad組合還是比較普遍的。Monad Transformer本身就是Monad,可以繼續與另一個Monad組合,只要用這個Monad的Transformer就行了。例如我們在上面的例子里再增加一層State,最終形成一個三層類型:State[Either[Option[A]]]。按照上面的經驗,堆砌Monad是由內向外的,我們先組合 StateEither >>> StateT[Either,A],然後再得出組合:OptionT[StateEither,A]。我們來示範一下:
先重新命名(alias)一些類:
type StringEither[A] = String \/ A
type StringEitherT[M[_],A] = EitherT[M,String,A]
type IntState[A] = State[Int,A]
type IntStateT[M[_],A] = StateT[M,Int,A]
type StateEither[A] = StringEitherT[IntState,A]
type StateEitherOption[A] = OptionT[StateEither,A]
由Option,Either,State組合而成的Monad需要相關的升格函數(lifting functions):
//常量升格
val m: StateEitherOption[Int] = 3.point[StateEitherOption]
//> m : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz.p
//| ackage$StateT$$anon$1@4f638935))
//option類升格
val o: Option[Int] = 3.some //> o : Option[Int] = Some(3)
val o1: StateEither[Option[Int]]= o.point[StateEither]
//> o1 : Exercises.monad_txnfm.StateEither[Option[Int]] = EitherT(scalaz.packag
//| e$StateT$$anon$1@694abbdc)
val o2: StateEitherOption[Int] = OptionT.optionT(o1)
//> o2 : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz.
//| package$StateT$$anon$1@694abbdc))
//val o2: OptionT[StateEither,Int] = OptionT.optionT(o1)
//either類升格
val e: StringEither[Int] = 3.point[StringEither] //> e : Exercises.monad_txnfm.StringEither[Int] = \/-(3)
val e1: IntState[StringEither[Int]] = e.point[IntState]
//> e1 : Exercises.monad_txnfm.IntState[Exercises.monad_txnfm.StringEither[Int]
//| ] = scalaz.package$StateT$$anon$1@52bf72b5
val e2: StateEither[Int] = EitherT.eitherT(e1) //> e2 : Exercises.monad_txnfm.StateEither[Int] = EitherT(scalaz.package$StateT
//| $$anon$1@52bf72b5)
//val e2: StringEitherT[IntState,Int] = EitherT.eitherT(e1)
val e3: StateEitherOption[Int] = e2.liftM[OptionT]//> e3 : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz.
//| IndexedStateT$$anon$10@2d7275fc))
//val e3: OptionT[StateEither,Int] = e2.liftM[OptionT]
//state類升格
val s: IntState[Int] = get[Int] //> s : Exercises.monad_txnfm.IntState[Int] = scalaz.package$State$$anon$3@7e0
//| 7db1f
val s1: StateEither[Int] = s.liftM[StringEitherT] //> s1 : Exercises.monad_txnfm.StateEither[Int] = EitherT(scalaz.IndexedStateT
//| $$anon$10@8f4ea7c)
//val s1: StringEitherT[IntState,Int] = s.liftM[StringEitherT]
val s2: StateEitherOption[Int] = s1.liftM[OptionT]//> s2 : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz
//| .IndexedStateT$$anon$10@436813f3))
//val s2: OptionT[StateEither,Int] = s1.liftM[OptionT]
//把State升格成StateT
val s3: IntStateT[StringEither,Int] = get[Int].lift[StringEither]
//> s3 : Exercises.monad_txnfm.IntStateT[Exercises.monad_txnfm.StringEither,In
//| t] = scalaz.IndexedStateT$$anon$7@10e31a9a
上面又多介紹了StateT.lift, EitherT.eitherT兩個升格函數:
def lift[M[_]: Applicative]: IndexedStateT[({type λ[α]=M[F[α]]})#λ, S1, S2, A] = new IndexedStateT[({type λ[α]=M[F[α]]})#λ, S1, S2, A] {
def apply(initial: S1): M[F[(S2, A)]] = Applicative[M].point(self(initial))
}
...
trait EitherTFunctions {
def eitherT[F[_], A, B](a: F[A \/ B]): EitherT[F, A, B] = EitherT[F, A, B](a)
...
我們在上面例子的基礎上增加一層State效果後再試用一下這些升格函數:
1 def getString: Option[String] = "Hello ".some //> getString: => Option[String]
2 def getResult: StringEither[String] = "how are you!".right[String]
3 //> getResult: => Exercises.monad_txnfm.StringEither[String]
4 def modState(s:Int): IntState[Unit] = put(s) //> modState: (s: Int)Exercises.monad_txnfm.IntState[Unit]
5 val prg: StateEitherOption[String] = for {
6 s1 <- OptionT.optionT(getString.point[StateEither])
7 s2 <- "World,".point[StateEitherOption]
8 s3 <- (EitherT.eitherT(getResult.point[IntState]): StateEither[String]).liftM[OptionT]
9 _ <- (modState(99).liftM[StringEitherT]: StateEither[Unit]).liftM[OptionT]
10 } yield s1+s2+s3 //> prg : Exercises.monad_txnfm.StateEitherOption[String] = OptionT(EitherT(sc
11 //| alaz.IndexedStateT$$anon$10@158d2680))
12 prg.run //> res0: Exercises.monad_txnfm.StateEither[Option[String]] = EitherT(scalaz.In
13 //| dexedStateT$$anon$10@158d2680)
不錯,類型對了,prg可以通過編譯,但未免複雜了點。我花了許多時間去匹配這些類型,因為需要連續升格。可想而知,如果遇到四層以上的Monad組合,代碼會複雜成怎樣。其中重點還是在各種類型的升格。那我們還是回顧一下這些升格函數吧:
A.point[F[_]] >>> F[A] "hi".point[Option] = Option[String] = Some("hi")
M[A].liftM[T[_[_],_]] >>> T[M,A] List(3).liftM[OptionT] = OptionT[List,Int] = OptionT(List(Some(3)))
OptionT.optionT(M[Option[A]]) >>> OptionT[M,A] OptionT.optionT(List(3.some)) = OptionT[List,Int] = OptionT(List(Some(3)
EitherT.eitherT(M[Either[A]]) >>> EitherT[M,A] EitherT.eitherT(List(3.right[String])) = EitherT(List(\/-(3))
State.lift[M[A]] >>> StateT[M,A] get[Int].lift[Option] = StateT[Option,Int]
註意:以上採用了形象類型表述