Scalaz(10)- Monad:就是一種函數式編程模式-a design pattern


    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(...))))
在這里fa,fb,fc都是F[T]這樣的算法。可以看出當我們把flatMap串接起來后就形成了一個串型(sequencial)流程(workflow)的F[]算法。為了更清楚的了解串接flatMap的意義,我們用同等的for-comprehension來示范:
1 // for { 2 // a <- (fa: F[A]) 3 // b <- (fb: F[A]) 4 // c <- (fc: F[A]) 5 // } yield { ... }
這樣表達會更加清晰了:我們先運算fa,得到結果a后接着運算fb,得出結果b后再運算fc,得出結果c ... 這像是一段行令程序(imperative program)。我們再用個形象點的例子來示范說明:

 

 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

 

可以看出,每一個算法都依賴前面算法得出的結果。從這個例子我們可以得出Monad的串型運算(sequencial computation)特性。確切來說,flatMap並不適合並行運算,所以我們需要Applicative。這是因為Applicative是在既有的容器中運算,而flatMap則會重新創建新的容器(在Monad的世界里容器即為算法(computation)。但是因為我們講過Monad就是Applicative,所以Monad也可以實現並行運算。Applicative 的 ap 函數可以用 flatMap實現:
1 // ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a)))  
也可以用flatMap來實現Functor的map函數:

 

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如何能成為功能完善的編程語言。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM