深圳scala-meetup-20180902(3)- Using heterogeneous Monads in for-comprehension with Monad Transformer

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

  scala中的Option型別是個很好用的資料結構,用None來替代java的null可以大大降低程式碼的複雜性,它還是一個更容易解釋的狀態表達形式,比如在讀取資料時我們用Some(Row)來代表讀取的資料行Row,用None來代表沒有讀到任何資料,免去了null判斷。由此我們可以對資料庫操作的結果有一種很直觀的理解。同樣,我們又可以用Either的Right(Row)來代表成功運算獲取了結果Row,用Left(Err)代表運算產生了異常Err。對於資料庫程式設計我還是選擇了Task[Either[E,Option[A]]]這種型別作為資料庫操作運算的統一型別。可以看到這是一個複合型別:首先Task是一個non-blocking的運算結果型別,Either[E,Option[A]]則同時可以處理髮生異常、獲取運算結果、無法獲取結果幾種狀態。我覺著這樣已經足夠代表資料庫操作狀態了。

  在Task[Either[E,Option[A]]]這個複合型別中的組成型別Option[A],Either[E,A]實際上是包嵌A型別元素的不同管道,各自可以獨立支援Monadic程式設計,如下:

object session2 extends App {
  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

}

object session21 extends App {
  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)     //Left("oh no ...")

如果我們把這兩個型別在for-comprehension裡結合使用:

object session22 extends App {
  val ovalue: Option[Int] = Some(10)
  val evalue: Either[String,Int] = Right(10)

  val p = for {
    a <- ovalue
    b <- evalue
    c = a * b
  } yield c

  println(p)

}

Error:(39, 7) type mismatch;
 found   : scala.util.Either[String,Int]
 required: Option[?]
    b <- evalue

無法通過編譯!當然,這是因為Option,Either是不同的Monad。如果我們把這兩個Monad結合形成一個複合的型別,那麼用for-comprehension應該沒什麼問題,如下:

object session23 extends App {
  def combined(int i): Task[Either[String,Option[Int]] = ???
  val p = for {
    a <- combined(2)
    b <- combined(3)
    c = a * b
  } yield c

  println(p)  //Task(Right(5))

}

我們可能需要通過函式組合來構建這個複合型別。通過證明,Functor是可以實現函式組合的,如下:

object session4 extends App {
  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))
    }
  val optionInList = List(Some("1"),Some("22"),Some("333"))
  val optionInListFunctor = composeFunctor(Functor[List],Functor[Option])

  val strlen: String => Int = _.length
  println(optionInListFunctor.map(optionInList)(strlen))

}

//List(Some(1), Some(2), Some(3))

以上程式碼證明Functor[M]可以通過函式組合和Functor[N]形成Functor[M[N]]。好像這正是我們需要對兩個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 pure[A](a: => A) = ma.point(mb.pure(a))
        def bind[A,B](mab: M[N[A]])(f: A => M[N[B]]): M[N[B]] =
           ??? ...
  }

因為我們無法實現組合後的Monad特質函式bind,所以這條路走不通了。不過cats函式元件庫提供了OptionT,EitherT這兩個Monad Transformer,它們的型別款式如下:

final case class OptionT[F[_], A](value: F[Option[A]]) {...}
inal case class EitherT[F[_], A, B](value: F[Either[A, B]]) {...}

//包嵌型別
OptionT[Task,A] => Task[Option[A]]
EitherT[Task,A,B] => Task[Either[A,B]]

//多層套嵌
Task[Either[E,Option[A]]] => OptionT[EitherT[Task,E,A],A]

Monad Transformer包嵌的型別正是我們需要的型別,我們可以用Task來代表F[_]。實際上EitherT也可以被視為一種F[_],所以從OptionT[EitherT[Task,E,A],A]可以得到Task[Either[E,Option[A]]]。注意複合型Monad Transformer的組成是由內向外反向的:Option[A]是最內的元素,那麼在合成時就擺在最外。下面我們就用type定義簡化整個描述:

  type DBOError[A] = EitherT[Task,String,A]
  type DBOResult[A] = OptionT[DBOError,A]

這樣表示就清楚多了,這個DBOResult[A]就是我們需要對付的型別。剩下來的工作就是需要提供一些型別轉換函式,分別把A,Option[A],Either[String,A],Task[A]都轉換成DBOResult[A]:

  def valueToDBOResult[A](a: A) : DBOResult[A] =
    Applicative[DBOResult].pure(a)
  def optionToDBOResult[A](o: Option[A]): DBOResult[A] =
    OptionT(o.pure[DBOError])
  def eitherToDBOResult[A](e: Either[String,A]): DBOResult[A] = {
    val error: DBOError[A] = EitherT.fromEither[Task](e)
    OptionT.liftF(error)
  }
  def taskToDBOResult[A](task: Task[A]): DBOResult[A] = {
    val error: DBOError[A] = EitherT.liftF[Task,String,A](task)
    OptionT.liftF(error)
  }

都是些純純的幫助函式,一次定義了可以永久使用。下面就是一個具體應用的例子:

object session41 extends App {
  type DBOError[A] = EitherT[Task,String,A]
  type DBOResult[A] = OptionT[DBOError,A]

  def valueToDBOResult[A](a: A) : DBOResult[A] =
    Applicative[DBOResult].pure(a)
  def optionToDBOResult[A](o: Option[A]): DBOResult[A] =
    OptionT(o.pure[DBOError])
  def eitherToDBOResult[A](e: Either[String,A]): DBOResult[A] = {
    val error: DBOError[A] = EitherT.fromEither[Task](e)
    OptionT.liftF(error)
  }
  def taskToDBOResult[A](task: Task[A]): DBOResult[A] = {
    val error: DBOError[A] = EitherT.liftF[Task,String,A](task)
    OptionT.liftF(error)
  }

  def task[T](t: T): Task[T] = Task.delay(t)
  def add(a: Int, b: Int): Task[Int] = Task.delay(a + b)

  val calc: DBOResult[Int] = for {
    a <- valueToDBOResult(10)
    b <- optionToDBOResult(Some(3))  //None: Option[Int])
    c <- eitherToDBOResult(Left[String,Int]("oh my good ..."))
    d <- taskToDBOResult(add(b,c))
  } yield d

  val sum: Task[Either[String,Option[Int]]] = calc.value.value

  import monix.execution.Scheduler.Implicits.global
  import scala.util._
  sum.runOnComplete {
    case Success(s) => println(s"DBOResult sum=$s")
    case Failure(exception) => println(exception.getMessage)
  }

}

//DBOResult sum=Left(oh my good ...)

從這段程式碼的運算結果可以確定:複合Monad Transformer的效果是它的組成Monad效果的疊加。在上面這個例子裡我們分別可以用None,Left來中斷運算,產生break一樣的效果。

 

相關文章