深圳scala-meetup-20180902(1)- Monadic 程式設計風格

雪川大蟲發表於2018-09-18

  剛完成了9月份深圳scala-meetup,趁刮颱風有空,把我在meetup裡的分享在這裡發表一下。我這次的分享主要分三個主題:“Monadic程式設計風格“、”Future vs Task and ReaderMonad應用方法“及”using heterogeneous monads in for-comprehension with MonadTransformer“。這篇想先介紹一下Monadic程式設計風格。

Monadic程式設計就是用Monad來程式設計,它的形式是:F[G],F是個Monad,然後G是具體的運算,G就是我們習慣的運算表示式如1+1、update(`a`,`new content`)等等,可能會產生副作用的,比如輸入輸出,更改資料等。形象點描述:如果我們把F[_]當作是一個管道,那麼Monadic程式設計模式就像是在F這個管道里組裝連線一些可能產生副作用的運算表示式。實際上真正產生運算結果的是管道內部的這些運算表示式。這是瘋了嗎?我們為什麼不直接按序運算這些表示式來獲取結果呢?我們先聽聽下面的分析:

看看下面這段程式:

行令程式設計模式(imperative programming)
def au(t:T): T      async update with result
val t2 = au(t1)
val t3 = au(t2)
val t4 = au(t2 + t3)         t4 = ???

如果上面每一行指令都在不同的執行緒裡運算,那麼完成運算的順序就是不確定的。最後t4的結果是不可預料的了。為了保證這個運算順序,我們可能要使用鎖,這又回到在OO程式設計裡最棘手的問題:執行低效、死鎖、難以理解跟蹤等。基本上OO程式設計的多執行緒程式不但難以理解而且運算難以捉摸,結果難以預覽,很難做的對。我們再看看Monadic程式設計:

monadic programming : program with monads
val fp3 = F[p1] ⊕ F[p1] ⊕ F[p1] = F[p1+p2+p3] 
1、延遲運算 :val res = fp3.run
2、按序運算 :flatMap{a => flatMap{b => flatMap{c =>… 

我們看到:所謂的Monadic程式設計就是在F[_]管道內運算式p1,p2,p3的連線。這樣做可以達到延遲運算和按序運算兩個主要目的。延遲運算可以讓我們完成對所有運算表示式的組合再一次性進行完整的運算。按序運算可以保證運算是按照程式設計人員的意圖進行的,這裡的flatMap是一種函式鏈,運算得到a後再運算b,得到b後再繼續運算c 。。。

下面是我們自創的一個F[_]結構Tube[A]和它的使用示範:

 case class Tube[A](run: A) {
    def map[B](f: A => B): Tube[B] = Tube(f(run))
    def flatMap[B](f: A => Tube[B]): Tube[B] = f(run)
  }

  val value: Tube[Int] = Tube(10)
  def add(a: Int, b: Int): Tube[Int] = Tube(a+b)

  val f = for {
    a <- value
    b <- add(a , 3)
    c <- add(a,b)
  } yield c

  println(f)          //Tube(23)
  println(f.run)      //23

首先,Tube[A]是個Monad,因為它支援map和flatMap。對任何Tube型別我們都可以用for-comprehension來組合運算式,最後run來獲取運算結果。以上a,b,c都是中間結果,可以在for{…}中任意使用。

值得注意的是:Monadic操作與scala裡集合的操作很相似,不同的是Monadic操作型別只包含一個內部元素,而集合包含了多個元素,如List(1,2,3)有3個元素。

實際上,簡單的一個Tube結構好像沒什麼特別用處,說白了它連中途終止運算的功能都沒有。scala庫裡現成的Monad中Option,Either都有特別的作用:Option可以在遇到None值時中斷運算並立即返回None值。Either在遇到Left值時立即返回Left,如下:

  val value: Option[Int] = Some(10)
  def add(a: Int, b: Int): Option[Int] = Some(a+b)

  val p = for {
    a <- value
    b <- add(a, 3)
    _ <- None
    c <- add(a,b)
  } yield a

  println(p)     //None


  val value: Either[String,Int] = Right(10)
  def add(a: Int, b: Int): Either[String,Int] = Right(a+b)

  val p = for {
    a <- value
    b <- add(a, 3)
    _ <- Left("oh no ...")
    c <- add(a,b)
  } yield c

  println(p)  //oh no ...

好了,下面我們就用一個形象點的例子來示範Monadic程式設計風格:這是一個模擬資料庫操作的例子,我們用一個KVStore來模擬資料庫:

  class KVStore[K,V] {
    private val s = new ConcurrentHashMap[K,V]()
    def create(k: K, v: V): Future[Boolean] = Future.successful(s.putIfAbsent(k,v) == null)
    def read(k: K): Future[Option[V]] = Future.successful(Option(s.get(k)))
    def update(k: K, v: V): Future[Unit] = Future.successful(s.put(k,v))
    def delete(k: K): Future[Boolean] = Future.successful(s.remove(k) == null)
  }

對KVStore的操作函式都採用了Future作為結果型別,這樣可以實現non-blocking操作。Future是個Monad(雖然它不是一種純函式impure function, 這個我們後面再解釋),所以我們可以用for-comprehension來程式設計,如下:

 type FoodName = String
  type Quantity = Int
  type FoodStore = KVStore[String,Int]

  def addFood(food: FoodName, qty: Quantity )(implicit fs: FoodStore): Future[Unit] = for {
    current <- fs.read(food)
    newQty = current.map(cq => cq + qty ).getOrElse(qty)
    _ <-  fs.update(food, newQty)
  } yield ()

  def takeFood(food: FoodName, qty: Quantity)(implicit fs: FoodStore): Future[Quantity] = for {
    current <- fs.read(food)
    instock = current.getOrElse(0)
    taken = Math.min(instock,qty)
    left = instock - taken
    _ <- if (left > 0) fs.update(food,left) else fs.delete(food)
  } yield taken

  def cookSauce(qty: Quantity)(get: (FoodName,Quantity) => Future[Quantity],
                               put:(FoodName,Quantity) => Future[Unit]): Future[Quantity] = for {
    tomato <- get("Tomato",qty)
    veggie <- get("Veggie",qty)
    garlic <- get("Garlic", qty * 3)
    sauceQ = tomato / 2 + veggie * 3 / 2
    _ <- put("Sauce",sauceQ)
  } yield sauceQ

  def cookMeals(qty: Quantity)(get: (FoodName,Quantity) => Future[Quantity],
                               put: (FoodName,Quantity) => Future[Unit]): Future[Quantity] =
    for {
       pasta <- get("Pasta", qty)
       sauce <- get("Sauce", qty)
      _ <- get("Spice",10)

      meals = Math.min(pasta,sauce)
      _ <- put("Meal", meals)

    } yield meals

上面幾個操作函式都是Future型別的,具體的操作都包含在for{…}裡。我們看到:在for{…}裡可以產生中間結果、也可以直接寫運算表示式、也可以使用這些中間運算結果。for{…}裡的情景就像正常的行令式程式設計。然後我們又對這些操作函式進行組合:

   implicit val refrigerator = new FoodStore

   val shopping: Future[Unit] = for {
     _ <- addFood("Tomato", 10)
     _ <- addFood("Veggie", 15)
     _ <- addFood("Garlic", 42)
     _ <- addFood("Spice", 100)
     _ <- addFood("Pasta", 6)
   } yield ()

   val cooking: Future[Quantity] = for {
     _ <- shopping
     sauce <- cookSauce(10)(takeFood(_,_),addFood(_,_))
     meals <- cookMeals(10)(takeFood(_,_),addFood(_,_))
   } yield (meals)

   val todaysMeals = Await.result(cooking,3 seconds)

  println(s"we have $todaysMeals pasta meals for the day.")

最後組合成這個cooking monad, 然後一次性Await.result(cooking…)獲取最終結果。通過上面這個例子我們可以得到這麼一種對Monadic程式設計風格的感覺,就是:用for-comprehension來組合,組合、再組合,然後run(Await.result)獲取結果。

 

相關文章