Slide 1

Slide 1 text

ちょうどいい、EitherT w/ Future 2024-09-06 Scalaわいわい勉強会 Naoki Aoyama - @aoiroaoino

Slide 2

Slide 2 text

❖ Naoki Aoyama ❖ Twitter/GitHub: @aoiroaoino ❖ 実践Scala入門 共著者 ❖ Working: $ whoami

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

めっちゃ活用してる EitherT with Future を紹介したい

Slide 7

Slide 7 text

Agenda ❖ Introduction ➢ アプリケーションサービスあるある話 ❖ ちょうどいい、 Future[Either[E, A]] ➢ 非同期処理、正常系、準正常系、異常系 … 表現力が丁度いい ➢ でも、for 式でフラットに合成はできない ... ❖ EitherT で包んじゃえ ➢ EitherT とは? cats の実装をみてみよう ➢ なぜ for 式でフラットに合成できる? map, flatMap を詳しく ❖ Tips ➢ 実用上の細かい話、注意点など ❖ まとめ

Slide 8

Slide 8 text

語らないこと ❖ Monad, MonadTransformer の詳解(そもそもほぼ触れない) ❖ アプリケーションサービスの実装 (ベスト)プラクティス ❖ IO モナドや Eff など、他の解決策について

Slide 9

Slide 9 text

Introduction

Slide 10

Slide 10 text

前回のスライドより

Slide 11

Slide 11 text

どんな場面で使ってるの?(ざっくりイメージ) アダプタA アダプタB アダプタC ドメイン層 アプリケーション層

Slide 12

Slide 12 text

どんな場面で使ってるの?(ざっくりイメージ) アダプタA アダプタB アダプタC ドメイン層 アプリケーション層 アプリケーションサービス内で さまざまな処理の合成に使用 様々な処理を組み合わせて ユースケースを実装していく ❖ データを読み込んだり ❖ 外部連携サービスと同期したり ❖ イベント送信したり ❖ などなど

Slide 13

Slide 13 text

アプリケーションサービス実装例: 理想 // ユーザーの表示名を変更するユースケース(理想) 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(...) }

Slide 14

Slide 14 text

アプリケーションサービス実装例: 理想 // ユーザーの表示名を変更するユースケース(理想) 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]]  

Slide 15

Slide 15 text

アプリケーションサービス実装例: 理想 // ユーザーの表示名を変更するユースケース(理想) 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]] => 結果を適切にハンドリングしていく必要がある

Slide 16

Slide 16 text

アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) 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

Slide 17

Slide 17 text

アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) 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

Slide 18

Slide 18 text

アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) 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

Slide 19

Slide 19 text

アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) 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

Slide 20

Slide 20 text

アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) 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 式を ”フラット ”に 書けない

Slide 21

Slide 21 text

アプリケーションサービス実装例: 現実 // ユーザーの表示名を変更するユースケース(現実) 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 な 分岐に 都度対処

Slide 22

Slide 22 text

アプリケーションサービス実装例: 現実 ❖ for 式の中(特に Generator まわり)がごちゃごちゃしてつらい ❖ 処理が失敗した場合にどのような値が返るのか、ぱっと見わかりにくい ❖ 一度きりの実装なら...しかし、実際は大量のボイラープレートになる ❖ などなど

Slide 23

Slide 23 text

もう少し、整理してみよう

Slide 24

Slide 24 text

ちょうどいい、Future[Either[E, A]]

Slide 25

Slide 25 text

もう少し、型を整理してみる よくある処理の共通(表現力の高い)型、ありませんか? ❖ x: A … ❖ x: Either[E, A] … ❖ x: Future[Option[A]] … ❖ x: Future[A] … ❖ x: Nothing (throw Exception) …

Slide 26

Slide 26 text

もう少し、型を整理してみる よくある処理の共通(表現力の高い)型、ありませんか? ❖ 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)

Slide 27

Slide 27 text

もう少し、型を整理してみる よくある処理の共通(表現力の高い)型、ありませんか? ❖ x: A … ❖ x: Either[E, A] … ❖ x: Future[Option[A]] … ❖ x: Future[A] … ❖ x: Nothing (throw Exception) … Future[Either[E, A]] にしておけば よくあるパターンは表現できそう!

Slide 28

Slide 28 text

もう少し、型を整理してみる よくある処理の共通(表現力の高い)型の持つ意味を考える ❖ x: A … 成功系(成功のみ) ❖ x: Either[E, A] … 準正常系 or 正常系(異常系なし) ❖ x: Future[Option[A]] … 異常系 or 正常系(Opt な値とみなす場合) ❖ x: Future[A] … 異常系 or 正常系 ❖ x: Nothing (throw Exception) … 異常系

Slide 29

Slide 29 text

ちょうどいい表現力 Future[Either[E, A]] 準正常系 or 正常系を表現 非同期処理、異常系 or 正常系を表現

Slide 30

Slide 30 text

ちょうどいい、Future[Either[E, A]] ❖ 非同期処理を前提に、正常系/準正常系/異常系をシンプルに表現 ❖ 標準ライブラリのみで構成される ➢ 3rd party ライブラリが不要 ➢ Scala のバージョンが変わっても互換性がまず保たれる ❖ 学習コストの低さ、チーム開発における認知負荷の低さ ➢ Scala 入門の書籍、ドキュメントにもほぼ間違いなく解説がある ➢ 他の言語でも同様の実装があり、 (近年割と)一般的な概念でもある

Slide 31

Slide 31 text

でも 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

Slide 32

Slide 32 text

でも 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

Slide 33

Slide 33 text

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 して...

Slide 34

Slide 34 text

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) }

Slide 35

Slide 35 text

for 式で合成したければ Future[Either[E, A]] 型に map, flatMap があればいい !!

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

EitherT で包んじゃえ

Slide 39

Slide 39 text

EitherT とは ❖ 任意の型コンストラクタ F の作用と Either の失敗する可能性のある 計算効果を組み合わせられる MonadTransformer ➢ 型で表現するなら F[Either[A, B]] ➢ Scala、cats 特有の概念ではないことに注意 ❖ cats 版は cats.data.EitherT ➢ F[Either[A, B]] のデータ型(ラッパークラス)として実装されている

Slide 40

Slide 40 text

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] = ... // ... }

Slide 41

Slide 41 text

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)) } )

Slide 42

Slide 42 text

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 なら何もしない } )

Slide 43

Slide 43 text

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 })

Slide 44

Slide 44 text

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 })

Slide 45

Slide 45 text

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 になる ≒ 異常系で処理が短絡

Slide 46

Slide 46 text

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 の値を返し続ける ≒ 準正常系でも処理が短絡

Slide 47

Slide 47 text

先ほどの実例っぽいコードを 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

Slide 48

Slide 48 text

先ほどの実例っぽいコードを 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 (準正常系) で終了

Slide 49

Slide 49 text

先ほどの実例っぽいコードを 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 (準正常系) で終了

Slide 50

Slide 50 text

先ほどの実例っぽいコードを 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 準異常系は発生しないが、 例外が投げられたら異常系で終了

Slide 51

Slide 51 text

先ほどの実例っぽいコードを 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] を得る

Slide 52

Slide 52 text

先ほどの実例っぽいコードを 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 式でフラットに書けた! //

Slide 53

Slide 53 text

Tips

Slide 54

Slide 54 text

型アノテーション/型パラメータの明示が必要な場面がある ❖ 型パラメータを明示する必要がある ➢ Partially-Applied Type パターンが採用さ れてるので、指定しやすくはある ❖ 型推論がうまくいかず、型アノテーションを 付与することも ➢ ケースバイケースではある for { // ... newFoo <- EitherT.fromEither[Future]( fooFactory .create(...) .toEither .left .map(Result.Error(_): Result) ) // ... } yield { ... }

Slide 55

Slide 55 text

メソッドのシグネチャや返り値型に登場させない // 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]] = ...

Slide 56

Slide 56 text

まとめ

Slide 57

Slide 57 text

まとめ 👉 「Scalebase ペイメント」を Scala で開発しています 👉 Future[Either[E, A]] の表現力と利便性を共有した 👉 EitherT を用いて for 式でフラットに合成できる 👉 EitherT w/ Future はちょうどいい、よね?