Help us understand the problem. What is going on with this article?

Either[A, B], Future[T], Future[Option[A]]をEitherT[Future, A, B]へ統一する

More than 1 year has passed since last update.

背景

各層の戻り値の型がEither[A, B], Future[T], etcと混在しており混乱を極めている. Usecase層(DDDのApplication, CleanArchitectureのUseCase)で操作する各層の戻り値の型を統一して制御を楽にしたい. Scalazは初めて使った.

環境

  • OS: macOS Mojave Version 10.14.3
  • Scala: 2.12.8
  • Scalaz: 7.2.27
  • Framework: Play Framework 2.7.0
  • sbt: 1.2.8
  • IDE: IntelliJ IDEA 2018.3.3 (Community Edition)

EitherT[Future, A, B]ってなに? 何がうれしいの?

EitherTというのがある. "Eitherのモナドトランスフォーマー"らしい. 複数種のモナド(コンテキスト)をくっ付けて一種のモナド(コンテキスト)にできる, らしい. 一種のモナド(コンテキスト)なので一つのfor内容表記で統一的に処理できるのが利点. モナド(コンテキスト)毎に段階的に処理する必要はない.

eithert.scala
for {
  a <- FutureとEitherを一緒に判断するコンテキスト
  b <- FutureとEitherを一緒に判断するコンテキスト
} yield a + b

Scalazを使ってみる

EitherTはScalazの機能なので, 公式にならって依存ライブラリにScalazをとりあえず追加する. Scalazはまだ全然触っていないので質問されても答えられない.

build.sbt
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.27"

Converterを作ってみる

混乱を極めている各層の戻り値の方をEitherT[Future, A, B]に変換するためのConverterを定義する. UseCase層と各層の連携はcrossroad0201/ddd-on-scalaを参考にしている.

Either[DomainError, R] => EitherT[Future, UsecaseError, R]

Domain層(DDDのDomain, CleanArchitectureのEntity)が返すEitherを変換してみる. scalazにはEitherの反対側の型を一緒に定義できるscalaz.left[B]: (A \/ B), scalaz.right[B]: (B \/ A) がある.

either2eitherT.scala
import scalaz._
import Scalaz._
import scala.concurrent.{ExecutionContext, Future}

...

  implicit class DomainErrorOps[DE <: DomainError, R](domainResult: Either[DE, R]) {

    /**
      * Either[DE, R] => EitherT[Future, DE, R]
      * @param convertUsecaseError
      */
    def ifLeftThen(convertUsecaseError: DE => UsecaseError)(
        implicit ec:                    ExecutionContext): EitherT[Future, UsecaseError, R] =
      domainResult match {
        case Left(domainError) => EitherT(Future.successful { convertUsecaseError(domainError).left[R] })
        case Right(result)     => EitherT(Future.successful { result.right[UsecaseError] })
      }
  }

Future[R] => EitherT[Future, UsecaseError, R]

Slickとか, Slickとか, Slickが返すFuture[T]を変換してみる. Futureのエラーハンドリングはscala.concurrent.recoverを使用する.

future2eitherT.scala
import scalaz._
import Scalaz._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal

...

  implicit class InfraErrorOps[R](infraResult: Future[R]) {

    /**
      * Future[R] => EitherT[Future, UsecaseError, R]
      * @param convertUsecaseError
      */
    def ifFailureThen(convertUsecaseError: Throwable => UsecaseError)(
        implicit ec:                       ExecutionContext): EitherT[Future, UsecaseError, R] =
      EitherT(infraResult.map {
        _.right[UsecaseError]
      }.recover {
        case NonFatal(error) => convertUsecaseError(error).left[R]
      })
  }

Future[Option[R]] => EitherT[Future, UsecaseError, R]

Slickとか, Slickとか, Slickが返すFuture[Option[A]]を変換してみる. Optionが内包されているので, None, Someのケースを追加して考慮する.

futureOption2eitherT.scala
import scalaz._
import Scalaz._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal

...

  implicit class InfraOptionErrorOps[R](infraResult: Future[Option[R]]) {

    /**
      * Future[Option[R]] => EitherT[Future, UsecaseError, R]
      * @param usecaseError
      */
    def ifNotExists(usecaseError: => UsecaseError)(implicit ec: ExecutionContext): EitherT[Future, UsecaseError, R] =
      EitherT(infraResult.map {
        case None         => usecaseError.left[R]
        case Some(result) => result.right[UsecaseError]
      }.recover {
        case NonFatal(error) => usecaseError.left[R]
      })
  }

統一されたEitherT[Future, A, B]をUseCase層で操作してみる

Domain層の戻り値のEither[A, B], Infra層の戻り値のFuture[T], Future[Option[A]]がEitherT[Future, UsecaseError, R]に統一されたので, 一つのfor内包表記で処理できる.

usecase.scala
import scalaz.EitherT
import scalaz.std.scalaFuture._
import scala.concurrent.{ExecutionContext, Future}

...

    for {
      createXxxx  <- Xxxx.create(...) ifLeftThen xxxxInvalidParameterError // Either[DE, R] => EitherT[Future, UsecaseError, R]
      createdXxxx <- XxxxRepository.save(createXxxx.entity) ifFailureThen asUsecaseError // Future[R] => EitherT[Future, UsecaseError, R]
    } yield createdXxxx

Controllerで値を取り出してみる

ちなみにPlay2のControllerでJsonResponseを返す場合はこんな感じ.

controller.scala
import scalaz.std.scalaFuture._

...
        xxxxUsecase.createXxxx(...).fold(
          {
            case error: XxxxInvalidParameterError => BadRequest(error.errorCode)
            case error => InternalServerError(error.errorCode)
          },
          createdXxxx => Ok(createdXxxx.asJson)
        )

所感

\/ とか -\/ とか \/- とか書いたら強そう(意味不明)なイメージを持っていたけど, 書く機会がなかった.,, 関数型ライブラリはScalazとかCats(Circeとか)とかあるけど, 自分が使うのは統一した方がいいですかね. 型型するのはまだまだ先ですね.

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away