Подтвердить что ты не робот

Будущее [Вариант] в 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
}
4b9b3361

Ответ 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.