Будущее [Вариант] в Scala для-понимания
У меня есть две функции, которые возвращают фьючерсы. Я пытаюсь передать измененный результат от первой функции в другую, используя понимание for-yield.
Этот подход работает:
val schoolFuture = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- schoolStore.getSchool(sid.get) if sid.isDefined
} yield s
Однако я не доволен наличием "if" там, кажется, что я должен использовать карту вместо этого.
Но когда я пытаюсь с картой:
val schoolFuture: Future[Option[School]] = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- sid.map(schoolStore.getSchool(_))
} yield s
Я получаю ошибку компиляции:
[error] found : Option[scala.concurrent.Future[Option[School]]]
[error] required: scala.concurrent.Future[Option[School]]
[error] s <- sid.map(schoolStore.getSchool(_))
Я играл с несколькими вариантами, но не нашел ничего привлекательного, которое работает. Может ли кто-нибудь предложить лучшее понимание и/или объяснить, что случилось с моим вторым примером?
Ниже приведен минимальный, но полный пример выполнения с Scala 2.10:
import concurrent.{Future, Promise}
case class User(userId: Int)
case class UserDetails(userId: Int, schoolId: Option[Int])
case class School(schoolId: Int, name: String)
trait Error
class UserStore {
def getUserDetails(userId: Int): Future[Either[Error, UserDetails]] = Promise.successful(Right(UserDetails(1, Some(1)))).future
}
class SchoolStore {
def getSchool(schoolId: Int): Future[Option[School]] = Promise.successful(Option(School(1, "Big School"))).future
}
object Demo {
import concurrent.ExecutionContext.Implicits.global
val userStore = new UserStore
val schoolStore = new SchoolStore
val user = User(1)
val schoolFuture: Future[Option[School]] = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- sid.map(schoolStore.getSchool(_))
} yield s
}
Ответы
Ответ 1
Этот ответ на аналогичный вопрос о Promise[Option[A]]
может помочь. Просто замените Future
на Promise
.
Я вывел следующие типы для getUserDetails
и getSchool
из вашего вопроса:
getUserDetails: UserID => Future[Either[??, UserDetails]]
getSchool: SchoolID => Future[Option[School]]
Так как вы игнорируете значение отказа из Either
, вместо этого вместо Option
, вы фактически имеете два значения типа A => Future[Option[B]]
.
Как только у вас есть экземпляр Monad
для Future
(может быть один в scalaz, или вы можете напишите свое, как в ответе, который я связал), применение трансформатора OptionT
к вашей проблеме будет выглядеть примерно так:
for {
ud <- optionT(getUserDetails(user.userID) map (_.right.toOption))
sid <- optionT(Future.successful(ud.schoolID))
s <- optionT(getSchool(sid))
} yield s
Обратите внимание, что для сохранения совместимых типов ud.schoolID
завершается в (уже завершенное) будущее.
Результат этого для понимания будет иметь тип OptionT[Future, SchoolID]
. Вы можете извлечь значение типа Future[Option[SchoolID]]
с помощью метода трансформатора run
.
Ответ 2
(Отредактировано, чтобы дать правильный ответ!)
Ключ здесь состоит в том, что Future
и Option
не компонуются внутри for
, потому что не существует правильных подписи flatMap
. Напоминаем, что для десугаров:
for ( x0 <- c0; w1 = d1; x1 <- c1 if p1; ... ; xN <- cN) yield f
c0.flatMap{ x0 =>
val w1 = d1
c1.filter(x1 => p1).flatMap{ x1 =>
... cN.map(xN => f) ...
}
}
(где любая инструкция if
выдает a filter
в цепочку - я привел только один пример - и операторы равенства просто задают переменные перед следующей частью цепочки). Поскольку вы можете только flatMap
другие Future
s, каждое утверждение c0
, c1
,... за исключением последнего, лучше создать Future
.
Теперь getUserDetails
и getSchool
создают Futures
, но sid
является Option
, поэтому мы не можем поместить его в правую часть <-
. К сожалению, нет чистого готового способа сделать это. Если o
является опцией, мы можем
o.map(Future.successful).getOrElse(Future.failed(new Exception))
чтобы превратить Option
в уже завершенный Future
. Так
for {
ud <- userStore.getUserDetails(user.userId) // RHS is a Future[Either[...]]
sid = ud.right.toOption.flatMap(_.schoolId) // RHS is an Option[Int]
fid <- sid.map(Future.successful).getOrElse(Future.failed(new Exception)) // RHS is Future[Int]
s <- schoolStore.getSchool(fid)
} yield s
сделает трюк. Это лучше, чем у вас? Сомнительно. Но если вы
implicit class OptionIsFuture[A](val option: Option[A]) extends AnyVal {
def future = option.map(Future.successful).getOrElse(Future.failed(new Exception))
}
то внезапное понимание выглядит разумным снова:
for {
ud <- userStore.getUserDetails(user.userId)
sid <- ud.right.toOption.flatMap(_.schoolId).future
s <- schoolStore.getSchool(sid)
} yield s
Это лучший способ написать этот код? Возможно нет; он полагается на преобразование a None
в исключение просто потому, что вы не знаете, что еще делать в этой точке. Это трудно обойти из-за дизайнерских решений Future
; Я бы предположил, что ваш исходный код (который вызывает фильтр), по крайней мере, подходит для этого.
Ответ 3
Какое поведение вы бы хотели иметь в случае, если Option[School]
- None
? Вы хотите, чтобы будущее потерпело неудачу? С каким исключением? Хочешь, чтобы он никогда не заканчивался? (Это звучит как плохая идея).
В любом случае предложение if
в дескрипторах для выражения для вызова метода filter
. Таким образом, договор на Future#filter
таков:
Если текущее будущее содержит значение, которое удовлетворяет предикату, новое будущее также будет придерживаться этой ценности. В противном случае результат будущее не будет выполнено с помощью исключения NoSuchElementException.
Но подождите:
scala> None.get
java.util.NoSuchElementException: None.get
Как вы можете видеть, None.get возвращает то же самое.
Таким образом, избавление от if sid.isDefined
должно работать, и это должно вернуть разумный результат:
val schoolFuture = for {
ud <- userStore.getUserDetails(user.userId)
sid = ud.right.toOption.flatMap(_.schoolId)
s <- schoolStore.getSchool(sid.get)
} yield s
Имейте в виду, что результатом schoolFuture
может быть, например, scala.util.Failure[NoSuchElementException]
. Но вы не описали, какое другое поведение вам бы хотелось.
Ответ 4
Мы сделали небольшую обертку Future [Option [T]], которая действует как одна монада (никто даже не проверял ни один из законов монады, но есть карта, flatMap, foreach, filter и т.д.) - MaybeLater. Он ведет себя гораздо больше, чем асинхронный вариант.
Там много вонючего кода, но, возможно, это будет полезно, по крайней мере, в качестве примера.
Кстати: есть много открытых вопросов (здесь для примера.)
Ответ 5
Проще использовать https://github.com/qifun/stateless-future
или https://github.com/scala/async
для преобразования A-Normal-Form
.