Мой API все возвращает Future [Option [T]], как хорошо их сочетать в составе

Все мои методы API возвращают Future [Option [T]], пытаясь выяснить, как изящно выполнить следующее:

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

Следующий код в настоящее время не компилируется, потому что пользователь, местоположение и адрес - все Опция [Пользователь], Опция [Местоположение] и Опция [Адрес]

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

Я помню, что у scalaz есть OptionT, но я никогда не использовал его раньше и не уверен, как применить его к моей ситуации.

Если пользователь, местоположение или адрес фактически возвращают None, что произойдет при использовании OptionT, когда мне нужно применить его к 3-м моделям в этом случае?

Ответы

Ответ 1

Некоторые простые определения для полного рабочего примера:

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)
}

И ради полноты, как бы не выглядела реализация 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)
  )

т.е. мы должны встраиваться в понимание, так же, как нам нужно было бы вложить map, чтобы преобразовать, например. значение Int, которое может находиться внутри Future[Option[Int]].

Монадный трансформатор OptionT в Scalaz или Cats предназначен для работы с такими типами, как 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)

Или, если вы хотите 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

И затем:

scala> getProfile(1L, 1L, 1L).foreach(println)
Some(UserProfile(Foo McBar,The Moon,123 Moon St.))

Если любой из промежуточных результатов None, все это будет None:

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

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

И, конечно, если какой-либо из запросов завершится с ошибкой, все это не удастся с первой ошибкой.

В качестве сноски, если запросы не зависят друг от друга, вы можете их компоновать вместо монадически:

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

Это более точно вычисляет вычисление и может быть более эффективным, так как он может запускать запросы параллельно.