Future と Either の積み上げ 

モナド変換子の用例として度々取り上げられるものに Future データ型と Either の積み上げがある。 日本語で書かれたブログ記事として吉田さん (@xuwei_k) の Scala で Future と Either を組み合わせたときに綺麗に書く方法というものがある。

東京の外だとあまり知られていない話だと思うが、吉田さんは書道科専攻で、大学では篆書を書いたり判子を刻って (ほる? 何故か変換できない) いたらしい:

ハンドル名の由来となっている徐渭は明代の書・画・詩・詞・戯曲・散文の士で自由奔放な作風で有名だった。 これは吉田さんの関数型言語という書だ。

それはさておき、FutureEither を積み上げる必要が何故あるのだろうか? ブログ記事によるとこういう説明になっている:

  1. Future[A] は Scala によく現れる。
  2. future をブロックしたくないため、そこらじゅう Future だらけになる。
  3. Future は非同期であるため、発生したエラーを捕獲する必要がある。
  4. FutureThrowable は処理できるが、それに限られている。
  5. プログラムが複雑になってくると、エラー状態に対して自分で型付けしたくなってくる。
  6. FutureEither を組み合わせるには?

ここからが準備段階となる:

scala> :paste
// Entering paste mode (ctrl-D to finish)
case class User(id: Long, name: String)

// In actual code, probably more than 2 errors
sealed trait Error
object Error {
  final case class UserNotFound(userId: Long) extends Error
  final case class ConnectionError(message: String) extends Error
}
object UserRepo {
  def followers(userId: Long): Either[Error, List[User]] = ???
}
import UserRepo.followers

// Exiting paste mode, now interpreting.

defined class User
defined trait Error
defined object Error
defined object UserRepo
import UserRepo.followers

user がいて、twitter のようにフォローできて、「フォローしてる」「フォローされてる」という関係を保持するアプリを作るとします。

とりあえず今あるのは、followers という、指定された userId の follower 一覧を取ってくるメソッドです。 さて、このメソッドだけがあったときに 「あるユーザー同士が、相互フォローの関係かどうか?」 を取得するメソッドはどう書けばよいでしょうか?

答えも載っているので、そのまま REPL に書き出してみる。UserId 型だけは Long に変えた。

scala> def isFriends0(user1: Long, user2: Long): Either[Error, Boolean] =
         for {
           a <- followers(user1).right
           b <- followers(user2).right
         } yield a.exists(_.id == user2) && b.exists(_.id == user1)
isFriends0: (user1: Long, user2: Long)Either[Error,Boolean]

次に、データベース・アクセスか何かを非同期にするために followersFuture を返すようにする:

scala> :paste
// Entering paste mode (ctrl-D to finish)
import scala.concurrent.{ Future, ExecutionContext }
object UserRepo {
  def followers(userId: Long): Future[Either[Error, List[User]]] = ???
}
import UserRepo.followers

// Exiting paste mode, now interpreting.

import scala.concurrent.{Future, ExecutionContext}
defined object UserRepo
import UserRepo.followers

さてそうしたときに、isFriendsメソッドは、どのように書き換えればいいでしょうか?さて、これもすぐに正解だしてしまいます。 ただ、一応2パターンくらい出しておきましょう

scala> def isFriends1(user1: Long, user2: Long)
         (implicit ec: ExecutionContext): Future[Either[Error, Boolean]] =
         for {
           a <- followers(user1)
           b <- followers(user2)
         } yield for {
           x <- a.right
           y <- b.right
         } yield x.exists(_.id == user2) && y.exists(_.id == user1)
isFriends1: (user1: Long, user2: Long)(implicit ec: scala.concurrent.ExecutionContext)scala.concurrent.Future[Either[Error,Boolean]]

次のがこれ:

scala> def isFriends2(user1: Long, user2: Long)
         (implicit ec: ExecutionContext): Future[Either[Error, Boolean]] =
         followers(user1) flatMap {
           case Right(a) =>
             followers(user2) map {
               case Right(b) =>
                 Right(a.exists(_.id == user2) && b.exists(_.id == user1))
               case Left(e) =>
                 Left(e)
             }
           case Left(e) =>
             Future.successful(Left(e))
         }
isFriends2: (user1: Long, user2: Long)(implicit ec: scala.concurrent.ExecutionContext)scala.concurrent.Future[Either[Error,Boolean]]

これらの2つのバージョンの違いは何だろうか?

正常系の場合の動作は同じですが、followers(user1) がエラーだった場合の動作が異なります。

上記の for式を2回使ってる isFriends1 のほうでは、followers(user1) がエラーでも、 followers(user2) の呼び出しは必ず実行されます。

一方、isFriends2 のほうは、followers(user1) の呼び出しがエラーだと、followers(user2) は実行されません。

どちらにせよ、両方の関数も元のものに比べると入り組んだものとなった。 しかも増えた部分のコードは紋切型 (ボイラープレート) な型合わせをしているのがほとんどだ。 Future[Either[Error, A]] が出てくる全ての関数をこのように書き換えるのは想像したくない。

EitherT データ型 

Either のモナド変換子版である XorT データ型というものがある。

/**
 * Transformer for `Either`, allowing the effect of an arbitrary type constructor `F` to be combined with the
 * fail-fast effect of `Either`.
 *
 * `EitherT[F, A, B]` wraps a value of type `F[Either[A, B]]`. An `F[C]` can be lifted in to `EitherT[F, A, C]` via `EitherT.right`,
 * and lifted in to a `EitherT[F, C, B]` via `EitherT.left`.
 */
case class EitherT[F[_], A, B](value: F[Either[A, B]]) {
  ....
}

UserRepo.followers を仮実装してみると、こうなった:

scala> :paste
// Entering paste mode (ctrl-D to finish)
import cats._, cats.data._, cats.implicits._
object UserRepo {
  def followers(userId: Long)
    (implicit ec: ExecutionContext): EitherT[Future, Error, List[User]] =
    userId match {
      case 0L =>
        EitherT.right(Future { List(User(1, "Michael")) })
      case 1L =>
        EitherT.right(Future { List(User(0, "Vito")) })
      case x =>
        println("not found")
        EitherT.left(Future.successful { Error.UserNotFound(x) })
    }
}
import UserRepo.followers

// Exiting paste mode, now interpreting.

import cats._
import cats.data._
import cats.implicits._
defined object UserRepo
import UserRepo.followers

isFriends0 の書き換えをもう一度やってみる。

scala> def isFriends3(user1: Long, user2: Long)
         (implicit ec: ExecutionContext): EitherT[Future, Error, Boolean] =
         for{
           a <- followers(user1)
           b <- followers(user2)
         } yield a.exists(_.id == user2) && b.exists(_.id == user1)
isFriends3: (user1: Long, user2: Long)(implicit ec: scala.concurrent.ExecutionContext)cats.data.EitherT[scala.concurrent.Future,Error,Boolean]

素晴らしくないだろうか? 型シグネチャを変えて、あと ExecutionContext を受け取るようしたこと以外は、 isFriends3isFriends0 と同一のものだ。

実際に使ってみよう。

scala> implicit val ec = scala.concurrent.ExecutionContext.global
ec: scala.concurrent.ExecutionContextExecutor = scala.concurrent.impl.ExecutionContextImpl@321e273f

scala> import scala.concurrent.Await
import scala.concurrent.Await

scala> import scala.concurrent.duration._
import scala.concurrent.duration._

scala> Await.result(isFriends3(0, 1).value, 1 second)
res0: Either[Error,Boolean] = Right(true)

最初のユーザが見つからない場合は、EitherT はショートするようになっている。

scala> Await.result(isFriends3(2, 3).value, 1 second)
not found
res34: cats.data.Xor[Error,Boolean] = Left(UserNotFound(2))

"not found" は一回しか表示されなかった。

StateTReaderTOption の例と違って、この XorT は様々な場面で活躍しそうな雰囲気だ。

今日はこれまで。

Contents