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一樣的效果。