Edited at

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


背景

各層の戻り値の型が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とか)とかあるけど, 自分が使うのは統一した方がいいですかね. 型型するのはまだまだ先ですね.


参考文献