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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一个会写诗的程序员的博客

从 Java 开始进入 Kotlin的世界: From Java to Kotlin: JKotlinor从 Java 开始进入 Kotlin的世界: From Java to Kotlin: JKo

在前面的内容里,我们已经看到了Java与Kotlin的互操作的基本方式。为了更好的认识Java与Kotlin这两门语言,我们在这里给出一些基本功能,同时使用Ja...

1052
来自专栏ACM算法日常

String Problem(KMP+最小表示法)- HDU 3374

Give you a string with length N, you can generate N strings by left shifts. For ...

792
来自专栏猿人谷

编程小技巧

1.判断一个自然数是否是某个数的平方?(其实就是判断这个数一定是奇数相加的) 由于 (n+1)^2 =n^2 + 2n + 1, = ... = 1 +...

21510
来自专栏吴伟祥

单元测试的利器 Jmockdata 原

Jmockdata是一款实现模拟JAVA类型或对象的实例化并随机初始化对象的数据的工具框架。

2464
来自专栏算法修养

PAT 甲级 1104 sum of Number Segments

1104. Sum of Number Segments (20) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16...

2795
来自专栏陈树义

注解的那些事儿(三)| 注解的使用

在上面的 SweetDemo 中会发现我们在使用 @Sweet 注解的时候,手动给 sweetLevel 属性赋值。如果没有赋值,那么会报错。

1112
来自专栏小灰灰

java之通过反射生成并初始化对象

java之通过反射生成并初始化对象 在博文 《java之的读取文件大全》 中读取csv文件后,需要自己将csv文件的对象转为自己的DO对象,那么有没有办法我...

1K6
来自专栏Ryan Miao

jackson简单使用,对象转json,json转对象,json转list

添加jackson依赖: // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/ja...

40811
来自专栏大前端_Web

ECMAScript 6笔记(Symbol, Proxy 和 Reflect)

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/articl...

1052
来自专栏算法修养

POJ-2329 Nearest number - 2(BFS)

Nearest number - 2 Time Limit: 5000MS Memory Limit: 65536K Total Submis...

2444

扫码关注云+社区

领取腾讯云代金券