我的API都返回Future [Option [T]],如何在for-compr中很好地结合它们

Bla*_*man 14 scala scalaz

我的所有API方法都返回Future [Option [T]],试图找出如何优雅地执行以下操作:

case class UserProfile(user: User, location: Location, addresses: Address)
Run Code Online (Sandbox Code Playgroud)

以下代码目前无法编译,因为用户,位置和地址都是选项[用户],选项[位置]和选项[地址]

val up = for {
 user <- userService.getById(userId)
 location <- locationService.getById(locationId)
 address <- addressService.getById(addressId)
} yield UserProfile(user, location, address)
Run Code Online (Sandbox Code Playgroud)

我记得scalaz有OptionT,但我以前从未真正使用它,也不确定如何将它应用到我的情况中.

如果说用户,位置或地址实际上返回None,那么当我需要在这种情况下将它应用于3个模型时使用OptionT会发生什么?

Tra*_*own 21

为完整的工作示例,一些简单的定义:

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

type User = String
type Location = String
type Address = String

case class UserProfile(user: User, location: Location, addresses: Address)

def getUserById(id: Long): Future[Option[User]] = id match {
  case 1 => Future.successful(Some("Foo McBar"))
  case _ => Future.successful(None)
}

def getLocationById(id: Long): Future[Option[Location]] = id match {
  case 1 => Future.successful(Some("The Moon"))
  case _ => Future.successful(None)
}

def getAddressById(id: Long): Future[Option[Address]] = id match {
  case 1 => Future.successful(Some("123 Moon St."))
  case _ => Future.successful(None)
}
Run Code Online (Sandbox Code Playgroud)

为了完整起见,这是Scalaz免费实现的样子:

def getProfile(uid: Long, lid: Long, aid: Long): Future[Option[UserProfile]] =
  for {
    maybeUser     <- getUserById(uid)
    maybeLocation <- getLocationById(lid)
    maybeAddress  <- getAddressById(aid)
  } yield (
    for {
      user     <- maybeUser
      location <- maybeLocation
      address  <- maybeAddress
    } yield UserProfile(user, location, address)
  )
Run Code Online (Sandbox Code Playgroud)

也就是说,我们必须嵌套for-comprehensions,就像我们必须嵌套map来转换例如Int可能在a里面的值一样Future[Option[Int]].

OptionTScalaz或Cats中的monad变换器旨在允许您使用Future[Option[A]]没有此嵌套的类型.例如,您可以这样写:

import scalaz.OptionT, scalaz.std.scalaFuture._

def getProfile(uid: Long, lid: Long, aid: Long): OptionT[Future, UserProfile] =
  for {
    user     <- OptionT(getUserById(uid))
    location <- OptionT(getLocationById(lid))
    address  <- OptionT(getAddressById(aid))
  } yield UserProfile(user, location, address)
Run Code Online (Sandbox Code Playgroud)

或者,如果你想要一个,Future[Option[UserProfile]]你可以打电话run:

def getProfile(uid: Long, lid: Long, aid: Long): Future[Option[UserProfile]] = (
  for {
    user     <- OptionT(getUserById(uid))
    location <- OptionT(getLocationById(lid))
    address  <- OptionT(getAddressById(aid))
  } yield UserProfile(user, location, address)
).run
Run Code Online (Sandbox Code Playgroud)

然后:

scala> getProfile(1L, 1L, 1L).foreach(println)
Some(UserProfile(Foo McBar,The Moon,123 Moon St.))
Run Code Online (Sandbox Code Playgroud)

如果有任何中间结果None,整个过程将是None:

scala> getProfile(1L, 1L, 0L).foreach(println)
None

scala> getProfile(0L, 0L, 0L).foreach(println)
None
Run Code Online (Sandbox Code Playgroud)

当然,如果任何请求失败,整个事情就会因第一个错误而失败.

作为脚注,如果请求不依赖于彼此,您可以应用而不是单独地组合它们:

import scalaz.Scalaz._

def getProfile(uid: Long, lid: Long, aid: Long): Future[Option[UserProfile]] = (
  OptionT(getUserById(uid)) |@|
  OptionT(getLocationById(lid)) |@|
  OptionT(getAddressById(aid))
)(UserProfile.apply _).run
Run Code Online (Sandbox Code Playgroud)

这可以更准确地对计算进行建模,并且可以更高效,因为它可以并行运行请求.