Upgrade to Pro — share decks privately, control downloads, hide ads and more …

EitherT_with_Future

 EitherT_with_Future

【オフライン】Scalaわいわい勉強会 #3【東京】
https://scala-tokyo.connpass.com/event/325327/

発表資料です。

Naoki Aoyama - @aoiroaoino

September 06, 2024
Tweet

More Decks by Naoki Aoyama - @aoiroaoino

Other Decks in Technology

Transcript

  1. Agenda ❖ Introduction ➢ アプリケーションサービスあるある話 ❖ ちょうどいい、 Future[Either[E, A]] ➢

    非同期処理、正常系、準正常系、異常系 … 表現力が丁度いい ➢ でも、for 式でフラットに合成はできない ... ❖ EitherT で包んじゃえ ➢ EitherT とは? cats の実装をみてみよう ➢ なぜ for 式でフラットに合成できる? map, flatMap を詳しく ❖ Tips ➢ 実用上の細かい話、注意点など ❖ まとめ
  2. アプリケーションサービス実装例: 理想 // ユーザーの表示名を変更するユースケース(理想) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { user <- userRepository.findById(userId) updated = user.rename(newName) _ <- userRepository.store(updated) _ <- notificationService.notify(...) } yield { Result.Succes(...) }
  3. アプリケーションサービス実装例: 理想 // ユーザーの表示名を変更するユースケース(理想) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { user <- userRepository.findById(userId) updated = user.rename(newName) _ <- userRepository.store(updated) _ <- notificationService.notify(...) } yield { Result.Succes(...) } 現実は... ❖ DB からの取得 … def findById(...): Future[Option[A]] ❖ ドメインロジック実行 … def rename(...): Either[E, A] ❖ 外部サービス API Call … def notify(...): Future[Either[E, A]]  
  4. アプリケーションサービス実装例: 理想 // ユーザーの表示名を変更するユースケース(理想) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { user <- userRepository.findById(userId) updated = user.rename(newName) _ <- userRepository.store(updated) _ <- notificationService.notify(...) } yield { Result.Succes(...) } 現実は... ❖ DB からの取得 … def findById(...): Future[Option[A]] ❖ ドメインロジック実行 … def rename(...): Either[E, A] ❖ 外部サービス API Call … def notify(...): Future[Either[E, A]] => 結果を適切にハンドリングしていく必要がある
  5. アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { userOpt <- userRepository.findById(userId) result <- userOpt match { case Some(user) => user.rename(newName) match { case Right(updated) => userRepository.store(updated) case Left(error) => Future.successful(Result.Error(error)) } case None => Future.successful(Result.UserNotFound) } _ <- result match { case Result.Success => notificationService.run(...) case _ => Future.unit } } yield result
  6. アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { userOpt <- userRepository.findById(userId) result <- userOpt match { case Some(user) => user.rename(newName) match { // ユーザーが存在し、リネームに成功した場合のみ永続化 case Right(updated) => userRepository.store(updated) case Left(error) => Future.successful(Result.Error(error)) } case None => Future.successful(Result.UserNotFound) } _ <- result match { case Result.Success => notificationService.run(...) case _ => Future.unit } } yield result
  7. アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { userOpt <- userRepository.findById(userId) result <- userOpt match { case Some(user) => user.rename(newName) match { // ユーザーが存在し、リネームに成功した場合のみ永続化 case Right(updated) => userRepository.store(updated) case Left(error) => Future.successful(Result.Error(error)) } case None => // ユーザーが見つからないなら NotFound Future.successful(Result.UserNotFound) } _ <- result match { case Result.Success => notificationService.run(...) case _ => Future.unit } } yield result
  8. アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { userOpt <- userRepository.findById(userId) result <- userOpt match { case Some(user) => user.rename(newName) match { // ユーザーが存在し、リネームに成功した場合のみ永続化 case Right(updated) => userRepository.store(updated) case Left(error) => Future.successful(Result.Error(error)) } case None => // ユーザーが見つからないなら NotFound Future.successful(Result.UserNotFound) } _ <- result match { case Result.Success => notificationService.run(...) case _ => Future.unit // 成功していないなら何もしない } } yield result
  9. アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { userOpt <- userRepository.findById(userId) result <- userOpt match { case Some(user) => user.rename(newName) match { // ユーザーが存在し、リネームに成功した場合のみ永続化 case Right(updated) => userRepository.store(updated) case Left(error) => Future.successful(Result.Error(error)) } case None => // ユーザーが見つからないなら NotFound Future.successful(Result.UserNotFound) } _ <- result match { case Result.Success => notificationService.run(...) case _ => Future.unit // 成功していないなら何もしない } } yield result for 式を ”フラット ”に 書けない
  10. アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name): Future[Result]

    = for { userOpt <- userRepository.findById(userId) result <- userOpt match { case Some(user) => user.rename(newName) match { // ユーザーが存在し、リネームに成功した場合のみ永続化 case Right(updated) => userRepository.store(updated) case Left(error) => Future.successful(Result.Error(error)) } case None => // ユーザーが見つからないなら NotFound Future.successful(Result.UserNotFound) } _ <- result match { case Result.Success => notificationService.run(...) case _ => Future.unit // 成功していないなら何もしない } } yield result Explicit な 分岐に 都度対処
  11. もう少し、型を整理してみる よくある処理の共通(表現力の高い)型、ありませんか? ❖ x: A … Future.successful(Right(x)) ❖ x: Either[E,

    A] … Future.successful(x) ❖ x: Future[Option[A]] … x.map(_.toRight(leftValue)) ❖ x: Future[A] … x.map(a => Right(a)) ❖ x: Nothing (throw Exception) … Future.failed(x)
  12. もう少し、型を整理してみる よくある処理の共通(表現力の高い)型、ありませんか? ❖ x: A … ❖ x: Either[E, A]

    … ❖ x: Future[Option[A]] … ❖ x: Future[A] … ❖ x: Nothing (throw Exception) … Future[Either[E, A]] にしておけば よくあるパターンは表現できそう!
  13. もう少し、型を整理してみる よくある処理の共通(表現力の高い)型の持つ意味を考える ❖ x: A … 成功系(成功のみ) ❖ x: Either[E,

    A] … 準正常系 or 正常系(異常系なし) ❖ x: Future[Option[A]] … 異常系 or 正常系(Opt な値とみなす場合) ❖ x: Future[A] … 異常系 or 正常系 ❖ x: Nothing (throw Exception) … 異常系
  14. ちょうどいい、Future[Either[E, A]] ❖ 非同期処理を前提に、正常系/準正常系/異常系をシンプルに表現 ❖ 標準ライブラリのみで構成される ➢ 3rd party ライブラリが不要

    ➢ Scala のバージョンが変わっても互換性がまず保たれる ❖ 学習コストの低さ、チーム開発における認知負荷の低さ ➢ Scala 入門の書籍、ドキュメントにもほぼ間違いなく解説がある ➢ 他の言語でも同様の実装があり、 (近年割と)一般的な概念でもある
  15. でも for 式でフラットに合成できない... // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name):

    Future[Result] = for { userE <- userRepository.findById(userId) resultE <- userE match { case Right(user) => for { updatedE <- user.rename(newName) result <- updatedE match { case Right(updated) => userRepository.store(updated).map(_ => Result.Success) case Left(error) => Future.successful(Left(Result.OpsError(error))) } } yield result case Left(_) => Future.successful(Left(Result.UserNotFound)) } _ <- resultE match { case Right(Result.Success) => notificationService.run(...) case Left(Result.OpsError(_)) => Future.successful(()) case Left(Result.UserNotFound) => Future.successful } } yield result
  16. でも for 式でフラットに合成できない... // ユーザーの表示名を変更するユースケース(現実) def execute(userId: UserId, newName: User.Name):

    Future[Result] = for { userE <- userRepository.findById(userId) resultE <- userE match { case Right(user) => for { updatedE <- user.rename(newName) result <- updatedE match { case Right(updated) => userRepository.store(updated).map(_ => Result.Success) case Left(error) => Future.successful(Left(Result.OpsError(error))) } } yield result case Left(_) => Future.successful(Left(Result.UserNotFound)) } _ <- resultE match { case Right(Result.Success) => notificationService.run(...) case Left(Result.OpsError(_)) => Future.successful(()) case Left(Result.UserNotFound) => Future.successful } } yield result
  17. flatMap で書き直したらパターンが見える...? userRepository.findById(userId).flatMap { case Right(user) => user.rename(newName).flatMap { case

    Right(updated) => userRepository.store(updated).map(_ => Result.Success) case Left(error) => Future.successful(Left(Result.OpsError(error))) } case Left(_) => Future.successful(Left(Result.UserNotFound)) } Future#flatMap して、Either をパターンマッチ、 Future#flatMap して、Either をパターンマッチ、 Future#map して...
  18. Tips: Scala の for 式は map, flatMap に展開される fa.flatMap {

    a => fb.flatMap { b => fc.map { c => (a, b, c) } } } for { a <- fa b <- fb c <- fc } yield { (a, b, c) }
  19. for 式を”フラット”に合成するためのソリューション ❖ Free Monad や Eff など、eDSL でプログラムを組み立てる ❖

    Tagless-final で F[_]: Monad の制約をかける ❖ cats.effect.IO や zio.ZIO など、いわゆる高機能な IO 型に揃える ❖ cats.data.ContT のような継続モナドに揃える ❖ 合成したい時だけ EitherT[Future, A, B] に包む
  20. for 式を”フラット”に合成するためのソリューション ❖ Free Monad や Eff など、eDSL でプログラムを組み立てる ❖

    Tagless-final で F[_]: Monad の制約をかける ❖ cats.effect.IO や zio.ZIO など、いわゆる高機能な IO 型に揃える ❖ cats.data.ContT のような継続モナドに揃える ❖ 合成したい時だけ EitherT[Future, A, B] に包む
  21. EitherT とは ❖ 任意の型コンストラクタ F の作用と Either の失敗する可能性のある 計算効果を組み合わせられる MonadTransformer

    ➢ 型で表現するなら F[Either[A, B]] ➢ Scala、cats 特有の概念ではないことに注意 ❖ cats 版は cats.data.EitherT ➢ F[Either[A, B]] のデータ型(ラッパークラス)として実装されている
  22. EitherT とは ※ https://github.com/typelevel/cats/blob/v2.12.0/core/src/main/scala/cats/data/EitherT.scala より final case class EitherT[F[_], A,

    B](value: F[Either[A, B]]) { // ... def map[D](f: B => D)(implicit F: Functor[F]): EitherT[F, A, D] = ... def flatMap[AA >: A, D](f: B => EitherT[F, AA, D])(implicit F: Monad[F]): EitherT[F, AA, D] = ... // ... } object EitherT extends EitherTInstances { // ... final def pure[F[_], A]: PurePartiallyApplied[F, A] = ... final def fromEither[F[_]]: FromEitherPartiallyApplied[F] = ... // ... }
  23. EitherT の map, flatMap をみてみる def map[D](f: B => D)(implicit

    F: Functor[F]): EitherT[F, A, D] = bimap(identity, f) def bimap[C, D](fa: A => C, fb: B => D)(implicit F: Functor[F]): EitherT[F, C, D] = EitherT( F.map(value) { case Right(b) => Right(fb(b)) case Left(a) => Left(fa(a)) } )
  24. EitherT の map, flatMap をみてみる def map[D](f: B => D)(implicit

    F: Functor[F]): EitherT[F, A, D] = bimap(identity, f) def bimap[C, D](fa: A => C, fb: B => D)(implicit F: Functor[F]): EitherT[F, C, D] = EitherT( F.map(value) { case Right(b) => Right(fb(b)) case Left(a) => Left(fa(a)) } ) // F を Future に固定するなら def map[D](f: B => D): EitherT[Future, A, D] = EitherT( value.map { case Right(b) => Right(f(b)) case Left(a) => Left(a) // Left なら何もしない } )
  25. EitherT の map, flatMap をみてみる def flatMap[AA >: A, D](f:

    B => EitherT[F, AA, D])(implicit F: Monad[F]): EitherT[F, AA, D] = EitherT(F.flatMap(value) { case l @ Left(_) => F.pure(EitherUtil.rightCast(l)) case Right(b) => f(b).value })
  26. EitherT の map, flatMap をみてみる def flatMap[AA >: A, D](f:

    B => EitherT[F, AA, D])(implicit F: Monad[F]): EitherT[F, AA, D] = EitherT(F.flatMap(value) { case l @ Left(_) => F.pure(EitherUtil.rightCast(l)) case Right(b) => f(b).value }) // F を Future に固定するなら def flatMap[AA >: A, D](f: B => EitherT[Future, AA, D]): EitherT[Future, AA, D] = EitherT(value.flatMap { case l @ Left(_) => Future.successful(EitherUtil.rightCast(l)) case Right(b) => f(b).value })
  27. EitherT の map, flatMap をみてみる def flatMap[AA >: A, D](f:

    B => EitherT[F, AA, D])(implicit F: Monad[F]): EitherT[F, AA, D] = EitherT(F.flatMap(value) { case l @ Left(_) => F.pure(EitherUtil.rightCast(l)) case Right(b) => f(b).value }) // F を Future に固定するなら def flatMap[AA >: A, D](f: B => EitherT[Future, AA, D]): EitherT[Future, AA, D] = EitherT(value.flatMap { case l @ Left(_) => Future.successful(EitherUtil.rightCast(l)) case Right(b) => f(b).value }) Future#flatMap 内で例外が投げられたら catch されて Failure になる ≒ 異常系で処理が短絡
  28. EitherT の map, flatMap をみてみる def flatMap[AA >: A, D](f:

    B => EitherT[F, AA, D])(implicit F: Monad[F]): EitherT[F, AA, D] = EitherT(F.flatMap(value) { case l @ Left(_) => F.pure(EitherUtil.rightCast(l)) case Right(b) => f(b).value }) // F を Future に固定するなら def flatMap[AA >: A, D](f: B => EitherT[Future, AA, D]): EitherT[Future, AA, D] = EitherT(value.flatMap { case l @ Left(_) => Future.successful(EitherUtil.rightCast(l)) case Right(b) => f(b).value }) Left になったら f を評価せず 常に Left の値を返し続ける ≒ 準正常系でも処理が短絡
  29. 先ほどの実例っぽいコードを EitherT w/ Future で // ユーザーの表示名を変更するユースケース( EitherT with Future)

    def execute(userId: UserId, newName: User.Name): Future[Result] = (for { user <- EitherT.fromOptionF( userRepository.findById(userId), Result.UserNotFound: Result ) updated <- Either.fromEither[Future]( user.rename(newName).left.map(e => Result.OpsError(e)) ) _ <- EitherT.right[Result]( userRepository.store(updated) ) _ <- EitherT.right[Result]( notificationService.run(...) ) } yield { Result.Succes(...) }).merge
  30. 先ほどの実例っぽいコードを EitherT w/ Future で // ユーザーの表示名を変更するユースケース( EitherT with Future)

    def execute(userId: UserId, newName: User.Name): Future[Result] = (for { user <- EitherT.fromOptionF( userRepository.findById(userId), Result.UserNotFound: Result ) updated <- Either.fromEither[Future]( user.rename(newName).left.map(e => Result.OpsError(e)) ) _ <- EitherT.right[Result]( userRepository.store(updated) ) _ <- EitherT.right[Result]( notificationService.run(...) ) } yield { Result.Succes(...) }).merge findById が None を返したら Result.UserNotFound (準正常系) で終了
  31. 先ほどの実例っぽいコードを EitherT w/ Future で // ユーザーの表示名を変更するユースケース( EitherT with Future)

    def execute(userId: UserId, newName: User.Name): Future[Result] = (for { user <- EitherT.fromOptionF( userRepository.findById(userId), Result.UserNotFound: Result ) updated <- Either.fromEither[Future]( user.rename(newName).left.map(e => Result.OpsError(e)) ) _ <- EitherT.right[Result]( userRepository.store(updated) ) _ <- EitherT.right[Result]( notificationService.run(...) ) } yield { Result.Succes(...) }).merge rename が Left を返したら Result.OpsError (準正常系) で終了
  32. 先ほどの実例っぽいコードを EitherT w/ Future で // ユーザーの表示名を変更するユースケース( EitherT with Future)

    def execute(userId: UserId, newName: User.Name): Future[Result] = (for { user <- EitherT.fromOptionF( userRepository.findById(userId), Result.UserNotFound: Result ) updated <- Either.fromEither[Future]( user.rename(newName).left.map(e => Result.OpsError(e)) ) _ <- EitherT.right[Result]( userRepository.store(updated) ) _ <- EitherT.right[Result]( notificationService.run(...) ) } yield { Result.Succes(...) }).merge 準異常系は発生しないが、 例外が投げられたら異常系で終了
  33. 先ほどの実例っぽいコードを EitherT w/ Future で // ユーザーの表示名を変更するユースケース( EitherT with Future)

    def execute(userId: UserId, newName: User.Name): Future[Result] = (for { user <- EitherT.fromOptionF( userRepository.findById(userId), Result.UserNotFound: Result ) updated <- Either.fromEither[Future]( user.rename(newName).left.map(e => Result.OpsError(e)) ) _ <- EitherT.right[Result]( userRepository.store(updated) ) _ <- EitherT.right[Result]( notificationService.run(...) ) } yield { Result.Succes(...) }).merge 全ての計算が正常系だった場合は EitherT[Future, Result, Result] になるので merge メソッドで Future[Result] を得る
  34. 先ほどの実例っぽいコードを EitherT w/ Future で // ユーザーの表示名を変更するユースケース( EitherT with Future)

    def execute(userId: UserId, newName: User.Name): Future[Result] = (for { user <- EitherT.fromOptionF( userRepository.findById(userId), Result.UserNotFound: Result ) updated <- Either.fromEither[Future]( user.rename(newName).left.map(e => Result.OpsError(e)) ) _ <- EitherT.right[Result]( userRepository.store(updated) ) _ <- EitherT.right[Result]( notificationService.run(...) ) } yield { Result.Succes(...) }).merge \\ EitherT に包んで、 for 式でフラットに書けた! //
  35. 型アノテーション/型パラメータの明示が必要な場面がある ❖ 型パラメータを明示する必要がある ➢ Partially-Applied Type パターンが採用さ れてるので、指定しやすくはある ❖ 型推論がうまくいかず、型アノテーションを

    付与することも ➢ ケースバイケースではある for { // ... newFoo <- EitherT.fromEither[Future]( fooFactory .create(...) .toEither .left .map(Result.Error(_): Result) ) // ... } yield { ... }
  36. メソッドのシグネチャや返り値型に登場させない // cats への依存が発生してしまう // 合成しないなら都度 value で unwrap する必要も

    def findById(userId: UserId): EitherT[Future, Result, User] = ... // 合成前提としても常に Future[Either[E, A]] は型が冗長(場合にはよる) def findById(userId: UserId): Future[Either[Result, User]] = ... // あくまで処理の結果をミニマムに表現し、必要な時だけ EitherT に包む def findById(userId: UserId): Future[Option[User]] = ...
  37. まとめ 👉 「Scalebase ペイメント」を Scala で開発しています 👉 Future[Either[E, A]] の表現力と利便性を共有した

    👉 EitherT を用いて for 式でフラットに合成できる 👉 EitherT w/ Future はちょうどいい、よね?