1. 概要

ScalaのFutureを使用すると、並行性を宣言型の方法で処理し、非同期プログラミングの複雑さを隠すことができます。 map a Future を実行して、同時に計算される値を変更できます。 しかし、失敗した Future はどうでしょうか? 失敗したものを新しい値にマッピングできますか? このチュートリアルでは、その方法を正確に説明します。

2. シナリオ

簡単な例から始めましょう。 有効な日付を指定して、天気を返す予測サービスを実装するとします。 まず、天気をモデル化できるオブジェクトが必要です。

sealed trait Weather
case object Sunny extends Weather
case object Cloudy extends Weather
case object Rainy extends Weather
case object Windy extends Weather
case object Snowy extends Weather
case object Foggy extends Weather

ここまでは順調ですね。 今、私たちのサービスを書きます。 私たちの予報サービスは、 Weather を直接返すのではなく、 Future[Weather]を返す必要があります。 実際、気象サービスは外部HTTPサービスを使用して気象情報を取得します。 インターネットを介した外部サービスへの呼び出しは、I / O の点で非常に高いコストがかかる可能性があり、応答を数ミリ秒(または数秒)待機します。

私たちは賢明な開発者であるため、HTTP応答を待つスレッドの時間を無駄にしたくありません。 だから、私たちは呼び出しを残します ExecutionContext 計算結果を未来。 簡単にするために、HTTPクライアントの簡単な実装を示します。

import scala.concurrent.ExecutionContext.Implicits.global

class HttpClient {
  def get(url: String): Future[String] =
    if (url.contains("2020-10-18"))
      Future("Sunny")
    else if (url.contains("2020-10-19"))
      Future("Windy")
    else {
      Future {
        throw new RuntimeException
      }
    }
}

この簡単な実装により、予測サービスの実行をガイドできます。 それでは、HttpClientを使用して天気を取得する予報サービスを実装するときが来ました。

3. Futureを回復する方法

3.1. 同期計算による回復

予測サービスを開発する最初の試みは非常に馬鹿げています。 実際、HttpClient.getメソッドを呼び出すだけです。

class WeatherForecastService(val http: HttpClient) {
  def forecast(date: String): Future[Weather] =
    http.get(s"http://weather.now/rome?when=$date")
}

ただし、このバージョンの WeatherForecastService の問題は、Failure値を処理するすべての責任をクライアントに任せることです。 ただし、天気予報サービスのクライアントは、天気情報のみを取得し、エラーを処理しない場合があります。

失敗した場合にサービスが以前に取得した値をクライアントに返すと便利です。 それでは、前の予測を保存するサービスに属性を追加しましょう。

var lastWeatherValue: Weather = Sunny

lastWeatherValue は可変であり、並行環境で競合状態を引き起こす可能性があるという事実には焦点を当てていません。

したがって、主な質問は次のとおりです。 Failureインスタンスで完了したFutureからどのように回復できますか? 幸い、Scala2.12はtransformメソッドをFutureAPIに導入しました。

def transform[S](f: (Try[T]) ⇒ Try[S]): Future[S]

基本的に、変換メソッドは、指定された関数をこのFutureの結果に適用することにより、新しいFutureを作成します。

実際、 Try 値を入力として受け入れる関数を使用すると、正常に完了したFutureと例外的に完了したFutureの両方を処理できます。 さらに、入力関数は別の Try 値を返します。これは、障害から回復するか、 Future 値を新しい値にマッピングするか、または単に障害状態のままにすることを決定できます。

シナリオに戻ると、以前に取得した予測を返すネットワークエラーから回復できます。

def forecast(date: String): Future[Weather] = {
  http.get(s"http://weather.now/rome?when=$date")
    .transform {
      case Success(result) =>
        val retrieved = Weather(result)
        lastWeatherValue = retrieved
        Try(retrieved)
      case Failure(exception) =>
        println(s"Something went wrong, ${exception.getMessage}")
        Try(lastWeatherValue)
    }
}

3.2. 非同期計算による回復

新しいFutureを返す非同期計算からの値を使用して回復する必要がある場合はどうなりますか? 繰り返しになりますが、 FutureAPIはtransformWithメソッドを提供するため、幸運です。

def transformWith[S](f: Try[T] => Future[S]): Future[S]

transformWithメソッドは、Futureを生成する指定された関数をこのFutureの結果に適用することにより、新しいFutureを作成します。

最終的に、このメソッドは、FutureタイプのflatMapのように機能します。これは、 Future が正常に完了し、Futureが例外的に完了した場合に機能します。 。

このシナリオでは、エラーが発生した場合に別のフォールバックサービスから予測を取得することで、transformWithメソッドを使用できます。

def forecast(date: String, fallbackUrl: String): Future[Weather] =
  http.get(s"http://weather.now/rome?when=$date")
    .transformWith {
      case Success(result) =>
        val retrieved = Weather(result)
        lastWeatherValue = retrieved
        Future(retrieved)
      case Failure(exception) =>
        println(s"Something went wrong, ${exception.getMessage}")
        http.get(fallbackUrl).map(Weather(_))
    }

したがって、プライマリ予測HTTPサーバーがダウンしている場合でも、予測サービスのユーザーは要求された情報を入手できるようになりました。

3.3. 古いバージョンのScalaで回復する方法

Scala 2.12より前は、transformメソッドには異なるシグネチャがありました。

def transform[S](s: (T) ⇒ S, f: (Throwable) ⇒ Throwable): Future[S]

上記の方法は、完成したFutureFutureを例外的に変換できる2つの関数を入力として受け取ります。 したがって、障害を例外とは異なる値に変換する方法はありませんでした。回避策を見つける必要があります。

実際、回避策は存在し、最初に map メソッドを呼び出してハッピーパスを処理し、次にどのrecoverメソッドを呼び出していました。 後者を使用すると、例外的に完了した Future を回復して、例外とは異なるものに変えることができます。

古いバージョンのScalaが提供するツールを使用して、予測メソッドの最初のバージョンを実装しましょう。

def forecastUsingMapAndRecover(date: String): Future[Weather] =
  http.get(s"http://weather.now/rome?when=$date")
    .map { result =>
      val retrieved = Weather(result)
      lastWeatherValue = retrieved
      retrieved
    }
    .recover {
      case e: Exception =>
        println(s"Something went wrong, ${e.getMessage}")
        lastWeatherValue
    }

さらに、transformWithの動作を模倣することもできます。 単純なmapを使用する代わりに、flatMapを使用する必要があります。 transformWith は、Futureを入力として返す関数を受け取ることを忘れないでください。 次に、どのrecoverWith メソッドを呼び出すことができます。これは、必要に応じて正確に動作します。

def forecastUsingFlatMapAndRecoverWith(date: String, fallbackUrl: String): Future[Weather] =
  http.get(s"http://weather.now/rome?when=$date")
    .flatMap { result =>
      val retrieved = Weather(result)
      lastWeatherValue = retrieved
      Future(retrieved)
    }
    .recoverWith {
      case e: Exception =>
        println(s"Something went wrong, ${e.getMessage}")
        http.get(fallbackUrl).map(Weather(_))
    }

4. 結論

このチュートリアルでは、Scalaが例外的に完了した非同期計算から回復するために提供する利用可能な手法をリストしました。 最初に、Scalaの新しいバージョンが提供するツールであるtransformおよびtransformWithメソッドを分析しました。 次に、2.12より前のバージョンのScalaで同じ動作を取得する方法を示しました。

いつものように、コードはGitHubから入手できます。