web-dev-qa-db-ja.com

Scala for-comprehensionsのFuture [Option]

Futuresを返す2つの関数があります。 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(_))

私はいくつかのバリエーションを試してみましたが、機能する魅力的なものは何も見つかりませんでした。誰かがより良い理解を提案したり、私の2番目の例の問題点を説明したりできますか?

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
}
36
Ryan Bair

この回答Promise[Option[A]]に関する同様の質問に役立つかもしれません。 FuturePromiseに置き換えてください。

getUserDetailsgetSchoolの次の型を質問から推測しています:

getUserDetails: UserID => Future[Either[??, UserDetails]]
getSchool: SchoolID => Future[Option[School]]

Eitherからの失敗値を無視して、代わりにOptionに変換するため、A => Future[Option[B]]タイプの2つの値が事実上存在します。

MonadFutureインスタンスを取得したら( scalaz にインスタンスが1つあるか、リンクした回答のように独自に作成できます)、次のように適用します。問題のOptionTトランスフォーマーは次のようになります。

for {
  ud  <- optionT(getUserDetails(user.userID) map (_.right.toOption))
  sid <- optionT(Future.successful(ud.schoolID))
  s   <- optionT(getSchool(sid))
} yield s

タイプの互換性を維持するために、ud.schoolIDは(すでに完了している)Futureにラップされていることに注意してください。

このfor-comprehensionの結果の型はOptionT[Future, SchoolID]になります。トランスフォーマーのrunメソッドを使用して、タイプFuture[Option[SchoolID]]の値を抽出できます。

13
Ben James

(正しい答えを与えるために編集されました!)

ここで重要なのは、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ステートメントはfilterをチェーンにスローします-例を1つだけ示しましたが、equalsステートメントは、チェーンの次の部分の前に変数を設定するだけです)。 flatMap他のFuturesしか使用できないため、すべてのステートメントc0c1、...を除いて、最後のステートメントはFutureを生成する方がよいでしょう。

現在、getUserDetailsgetSchoolはどちらもFuturesを生成しますが、sidOptionであるため、右側に配置できません-<-の片側残念ながら、これを行うためのクリーンな方法はありません。 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

これはこのコードを書くための最良の方法ですか?おそらくそうではありません。 Noneを例外に変換することに依存しています。その時点で他に何をすべきかわからないからです。 Futureの設計上の決定のため、これを回避するのは困難です。元のコード(フィルターを呼び出す)は、少なくともそれを行うための優れた方法であることをお勧めします。

22
Rex Kerr

Option[School]Noneの場合、どのような動作をしたいですか?未来を失敗させませんか?どんな例外がありますか?決して完了させませんか? (それは悪い考えのように聞こえます)。

とにかく、for式の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]のインスタンスになる可能性があることに注意してください。しかし、あなたはあなたが望む他のどのような振る舞いを述べていません。

8
stephenjudkins

1つのモナドのように動作するFuture [Option [T]]に小さなラッパーを作成しました(モナドの法則をチェックする人はいませんが、map、flatMap、foreach、filterなどがあります)- MaybeLater =。非同期オプションよりもはるかに多くの動作をします。

そこには臭いコードがたくさんありますが、少なくとも例としては役立つでしょう。ところで:多くの未解決の質問があります( ここ の例)

3
Alex Povar

https://github.com/qifun/stateless-futureまたはhttps://github.com/scala/asyncを使用してA-Normal-Form変換を行う方が簡単です。

1
jilen