Monad Transformers 实际作用(译文)

翻译自:Monad Transformers for the working programmer —— Gabriele Petronella

从解决具体问题着手介绍 Monad Transformers 作用

是这样的,你坐在桌子旁,喝着咖啡,准备写一点 Scala 代码。函数式编程并不像你想象的那样可怕,生活是美好的,你舒展自己的四肢,放松一会,开始构思这周需要完成的一个新特性。

像以前很多日子一样,你惊叹于 Scala 的简洁,苦恼 Scala 代码的一些奇怪 bug。写着写着,你遇到了一个奇怪的问题,for-comprehension 不能正常编译了。“没什么大不了的”,你像往常一样心里想着,“StackOverflow 上搜一搜就解决了”。

今天很不幸,你没有很快找到这个问题的答案。

首先,你考虑了 top one 的答案,它讲的是关于 数学的范畴论,很快就放弃了这个答案往下翻,但是你发现第二个,第三个…都是关于 Monad. Transformers. 的,听起来就让人害怕。

那么你的问题是什么?要什么新功能?

def findUserById(id: Long): Future[User] = ???
def findAddressByUser(user: User): Future[Address] = ???

看上去很优雅,Future代表了异步编程,它有个 flatMap函数,可以用 for-comprehension,完美。那么新功能就是:

def findAddressByUserId(id: Long): Future[Address] =
for {
user <- findUserById(id)
address <- findAddressByUser(user)
} yield address

突然你意识到不是每个 id 都能查到一个 User,于是修改成

def findUserById(id: Long): Future[Option[User]] = ???

同样不是每个用户都填写了 address

def findAddressByUser(user: User): Future[Option[Address]] = ???

现在的返回值是Future[Option[Address]]

def findAddressByUserId(id: Long): Future[Option[Address]] =
for {
user <- findUserById(id)
address <- findAddressByUser(user)
} yield address

很开心,还是很优雅,但是,编译失败了

error: type mismatch;
found : Option[User]
required: User
address <- findAddressByUser(user)

因为<-只是flatMap的语法糖,得到的是Option[User],于是你不得不改为传统的写法

def findAddressByUserId(id: Long): Future[Option[Address]] =
findUserById(id).flatMap {
case Some(user) => findAddressByUser(user)
case None => Future.successful(None)
}

看上去一点也不好,因为不像以前那样能清晰的看到业务逻辑。你想象中的样子应该是这样的

def findAddressByUserId(id: Long): Future[Option[Address]] =
for {
user <- userOption <- findUserById(id)
address <- addressOption <- findAddressByUser(user)
} yield address

一种优雅的调用两次flatMap的方式,可惜 StackOverflow 上没人提供这种方案。那么,我们遇到的问题本质是什么?为什么flatMap不能直接作用于Future[Option[X]]

亲爱的读者,请深呼吸,我们即将提到纯理论的东西,不要绝望,你需要的理论只有简单的两条,我保证。

  1. Functor 是一种带有map函数的结构
  2. Monad 是一种带有flatMap函数的结构

一些基础的范畴论知识可以帮助解决这个神秘的问题。

如果你有两个 Functors(F[A[_]]F[B[_]]),那么你就可以免费得到F[A[B[_]]]。这意味者你知道了A[_]B[_]的 map 函数,就可以知道A[B[_]]的 map 函数。但是,这对 flatMap 函数不适用,即你不能自动的从A[_]B[_]的flatMap 函数得到A[B[_]]的flatMap函数。这是一个已知的结论:Monads do not compose

那么让我们为Future[Option[A]]提供 map 和 flatMap 函数

case class FutOpt[A](value: Future[Option[A]]) {

def map[B](f: A => B): FutOpt[B] =
FutOpt(value.map(optA => optA.map(f)))
def flatMap[B](f: A => FutOpt[B]): FutOpt[B] =
FutOpt(value.flatMap(opt => opt match {
case Some(a) => f(a).value
case None => Future.successful(None)
}))
}

在我们的问题上使用它

def findAddressByUserId(id: Long): Future[Option[Address]] =
(for {
user <- FutOpt(findUserById(id))
address <- FutOpt(findAddressByUser(user))
} yield address).value

看上去不错,同样List[Option[A]]也可以

case class ListOpt[A](value: List[Option[A]]) {

def map[B](f: A => B): ListOpt[B] =
ListOpt(value.map(optA => optA.map(f)))
def flatMap[B](f: A => ListOpt[B]): ListOpt[B] =
ListOpt(value.flatMap(opt => opt match {
case Some(a) => f(a).value
case None => List(None)
}))
}

深入思考一下,似乎是通用的逻辑,我们不必要知道外层的类型是什么,只要它有 map 和 flatMap 函数,也不需要知道内层类型是什么。

我们先固定内层的类型为Option,就可以得到一个通用结构OptionT,这就是一个 Monad Transformer 类型。OptionT有两个类型参数,FAF是外层Monad,A是内层Monad(这里是Option)的内部类型。换而言之,OptionT[F, A]F[Option[A]]的扁平版本。

OptionT[F, A] is a flat version of F[Option[A]] which is a Monad itself

注意OptionT也是一个 monad,我们可以使用 for-comprehension。

事实上有很多库提供了各种各样的 Monad Transformers(OptionT,EitherT…),比如 cats,scalaz 等。

现在我们使用 cats 库试试,它就像这样

import cats.data.OptionT, cats.std.future._
def findAddressByUserId(id: Long): Future[Option[Address]] =
(for {
user <- OptionT(findUserById(id))
address <- OptionT(findAddressByUser(user))
} yield address).value

可以更优雅一点吗?我们把所有的返回值都改成OptionT[F, A]

def findUserById(id: Long): OptionT[Future, User] =  OptionT { ??? }
def findAddressByUser(user: User): OptionT[Future, Address] = OptionT { ??? }
def findAddressByUserId(id: Long): OptionT[Future, Address] =
for {
user <- findUserById(id)
address <- findAddressByUser(user)
} yield address

Nice!当我们需要使用真实值时,调用.value函数就可以了。

在结束之前,我还需要给你几点忠告:

  • Monad Transformers 很好,但是不要嵌套过多的层。因为会变的很复杂,更多的嵌套层可以参考 这个项目的README
  • Monad Transformers 并不是免费的,存在很多次 wrapping/unwrapping 操作,会带一定的性能开销。
  • 因为这不是标准库中的实现,不要把Monad Transformer 做为返回值提供给外部api,不能期望所有的使用者明白它的含义。

最后,Monad Transformer 只是解决层叠Monad方案中的一种,它比较简单,也不需要对代码坐过多修改,很容易使用,如果你做好准备学习更多,可以看这里 Eff

那么总结一下:Monad Transformer 通过提供一种 扁平化的Monad 代替 层叠的 Monad,来解决我们面临多层Monad 的问题。我希望我已经解释的足够清楚,让你不在害怕它。你可以在实际的工作中使用它,但是不要滥用。

如果你想了解的更多,下面是我在 Scala Days 2017 的关于这个问题的演讲视频


- - - - - - - - End Thank For Your Reading - - - - - - - -
0%