1. yu-croco

    Posted

    yu-croco
Changes in title
+Effを使ってDDDのUseCase層をいい感じに書いてみる
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,171 @@
+## 経緯
+Scala(PlayFramework) x DDDでアプリケーションを実装する際、UseCase層(Application層)を実装する際に辛さが出てくる。
+何が辛いかと言うと、型のネストである。
+
+というのも、
+
+- UseCase層ではエンティティ操作の過程で仕様周りのバリデーションをやることになりEitherが出てくる
+ - 例:ハンターがモンスターから素材を剥ぎ取るためには、モンスターが既に死んでいる必要がある
+- (PlayFrameworkだと特に)Repository層での呼び出してFutureが出てくる
+
+
+そのため、UseCase層での各処理の型合わせが必然的に複雑になる傾向にある。
+
+## サンプル
+例として、なんちゃってモンハンを想定して「ハンターがモンスターにダメージを与える」というユースケースを実装してみる。
+*いろんな突っ込みがあると思うのですが、マサカリはヤメてください。
+
+forの内包式でザッと書いてみるとこんな感じになる。
+色々自前で置いているが細かいところは置いておいて、それぞれのfor内での文脈(型)に着目してもらいたい。
+
+```scala:useCase層
+// UseCase層のメインメソッド
+def run(hunterId: HunterId, monsterId: MonsterId): Future[Either[UCError, Future[Monster]]] =
+ for {
+ // Futureのレーン
+ hunter <- hunterRepository.findById(hunterId).toUCErrorIfNotExists("hunter")
+ monster <- monsterRepository.findById(monsterId).toUCErrorIfNotExists("monster")
+ hunterAttackDamage = HunterAttackService.calculateDamage(hunter, monster)
+ } yield for {
+ // Eitherのレーン
+ damagedMonster <- hunter.attack(monster, hunterAttackDamage).toUCErrorIfLeft()
+ } yield for {
+ // Futureのレーン
+ savedMonster <- monsterRepository.update(damagedMonster).raiseIfFutureFailed("monster")
+ } yield savedMonster
+```
+
+今回のケースではFuture -> Either -> Futureの構成となり、そのまま`savedMonster`を返すと戻り値が`Future[Either[UCError, Future[Monster]]]`となる。
+ただ、実際には`Future[Either[UCError, Monster]]`にしたい。
+
+こうして型合わせゲームが始まる。
+
+またユースケースによっては`Future[Either[UseCaseError, Future[Option[Monster]]]]`とかになることもあり得るので、地獄感が溢れている。
+
+## Monad Transformerという希望
+この「ネストした型どうしよう問題」に対して、Monad Transformerというのが活躍する。
+Monad Transformerとは、ざっくりいうと「異なる種類のモナドインスタンスを合成して一つのモナドインスタンスとして扱えるようにするデータ型の総称」のことである。
+
+要は、`Either`や`Future`などを一つのモナドとして扱えるようにしてくれる代物である。
+
+有名なライブラリだと、catsやscalazが既に出してくれている。例えばEitherTがそれにあたる。
+
+- [cats EitherT](https://typelevel.org/cats/datatypes/eithert.html)
+- [scalaz EitherT](https://javadoc.io/static/org.scalaz/scalaz-core_2.13/7.2.29/scalaz/EitherT.html)
+
+
+魔法のようなことが出来るのだが、どうも欠点もあるようだ。
+![スクリーンショット 2020-08-28 20.05.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102733/11c293cd-cac0-1b4d-b969-9209f1518f79.png)
+*画像は[Extensible Effects with Scala/eff-with-scala](https://speakerdeck.com/hiroki6/eff-with-scala)のp.14から拝借
+
+
+## Eff現る
+上記の問題を解消するべく現れたのがEff(Extensible Effects)である。
+Effに関する詳細は、`参考`にリンクを貼っているので是非そちらを参照頂きたい。
+大枠としては以下の資料がわかりやすい。
+![スクリーンショット 2020-08-28 20.08.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102733/76c7d885-6ac3-79da-e33b-266c4bc7790a.png)
+![スクリーンショット 2020-08-28 20.08.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102733/b6df887b-1ee1-1ed5-572d-c333e34cd2de.png)
+![スクリーンショット 2020-08-28 20.09.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102733/2b793490-512f-dc87-d807-ea1e6388363f.png)
+*画像は[Extensible Effects with Scala/eff-with-scala](https://speakerdeck.com/hiroki6/eff-with-scala)のp.18~20から拝借
+
+## Effを使って書き直す
+Effを使って書き直すとどうなるかやってみる。
+*できるだけ説明は試みるが、より適切かつ詳しい情報は公式の[atnos-org eff Introduction](https://atnos-org.github.io/eff/org.atnos.site.Introduction.html)を読んで頂きたい
+
+Effでは上記の画像の説明の通り、モナドをスタックさせていく。
+スタックをさせるためにはEffを使いたいメソッドに対して、
+
+- 引数に型パラメータとして`[R: hoge: fuga]`と指定する
+ - hoge, fugaはスタックさせたいeffectに該当する
+ - 今回は`UCEither`と`Future`がそれに該当する
+- 戻り値として`Eff[R, A]`という型に
+ - `A`は最終的に返す型を表す
+ - 今回は`Monster`がそれに該当する
+
+
+型パラメータとして配置するeffectは、いくつかはeff側での組み込みが存在する(Futureに対応する`_future`など)が、`UCEither`のような自作のeffectの場合には`type _hoge[R] = Hoge |= R` という感じのものを定義し、Rの仲間入りを果たす必要がある。
+
+その後、forの中で既存の処理に対してeffを適応するためのメソッドを使ってeffectをスタックしていく。
+
+```scala:UseCase層
+// UseCase層用に自前のEitherがあるとする
+type UCEither[T] = Either[UCError, T]
+// effect(UCEither)をRのメンバーに加えてやる
+type _ucEither[R] = UCEither |= R
+
+// UCEitherとFutureをスタックして、Monsterを返す
+// ここではプログラムを組み立てるだけであり、この段階ではまだ処理は実行されないので、programというメソッド名に変える
+def program[R: _ucEither: _future](hunterId: HunterId, monsterId: MonsterId): Eff[R, Monster] =
+ for {
+ hunter <- fromFuture(hunterRepository.findById(hunterId).toUCErrorIfNotExists("hunter"))
+ monster <- fromFuture(monsterRepository.findById(monsterId).toUCErrorIfNotExists("monster"))
+ hunterAttackDamage = HunterAttackService.calculateDamage(hunter, monster)
+ damagedMonster <- either.fromEither(hunter.attack(monster, hunterAttackDamage).toUCErrorIfLeft())
+ savedMonster <- fromFuture(monsterRepository.update(damagedMonster).raiseIfFutureFailed("monster"))
+ } yield savedMonster
+```
+
+これでfor文一つで簡潔してしまった。
+なんと美しいことか。
+
+ちなみに、fromFutureとかで囲むのはちょっとダサいので、implicit classを用意してやるとより見栄えが良くなる。
+
+```scala:UseCase層(改良)
+object usecase {
+ implicit class FutureOps[T](futureValue: Future[T])(implicit ex: ExecutionContext) {
+ def toEff[R: _future]: Eff[R, T] = fromFuture(futureValue)
+ }
+
+ implicit class EitherUCErrorOps[T](eitherValue: Either[UseCaseError, T]) {
+ def toEff[R: _useCaseEither]: Eff[R, T] = either.fromEither(eitherValue)
+ }
+}
+
+def program[R: _future: _useCaseEither](hunterId: HunterId, monsterId: MonsterId): Eff[R, Hunter] =
+ for {
+ hunter <- hunterRepository.findById(hunterId).toUCErrorIfNotExists("hunter").toEff
+ monster <- monsterRepository.findById(monsterId).toUCErrorIfNotExists("monster").toEff
+ monsterAttackDamage = MonsterAttackService.calculateDamage(monster, hunter)
+ damagedHunter <- monster.attack(hunter, monsterAttackDamage).toUCErrorIfLeft().toEff
+ savedHunter <- hunterRepository.update(damagedHunter).raiseIfFutureFailed("hunter").toEff
+ } yield savedHunter
+```
+
+programメソッドによりeffectのスタックが完成したので、adapter層でそれを呼び出して実行する。
+この際、スタックしたeffectを一つずつ実行させて、`Future[Either[UCError, Monster]]`にしていく。
+なお、実行順序は呼び出し側で自由に指定できる。
+
+```scala:Adapter層
+// Rとしてスタックさせたいeffectをセットする(順番は気にしなくて良い)
+type Stack = Fx.fx2[UseCaseEither, TimedFuture]
+
+def update() = Action.async(parse.json) { implicit request =>
+ // bodyのパースとかいろいろ...
+ useCase
+ // Effectのスタックを組み上げる
+ .program[Stack](hunterId, monsterId)
+ .runEither[UCEither]
+ // ここまでくると、Future[Either[UCError, Monster]]に変換が完了する
+ .runAsync
+ .flatMap {
+ case Right(monster) => Future.successful(monster)
+ case Left(ucError) => Future.failed(ucError)
+ }
+ .toJsonResponse
+```
+
+詳細な挙動はお手元の環境にeffを入れて確認して見ると良いと思います。
+[atnos-org eff introduction](https://atnos-org.github.io/eff/org.atnos.site.Introduction.html)にも詳しく掲載されているので、そちらもどうぞ。
+
+## 所感
+- 型合わせゲームがこんなにも簡単に解決されるのは驚き!
+- その分魔法が凄いので、内部実装を追ってもう少し詳細な挙動を把握してみたい
+
+## 参考
+- [atnos-org eff](https://atnos-org.github.io/eff/index.html)
+- [Extensible Effects with Scala/eff-with-scala](https://speakerdeck.com/hiroki6/eff-with-scala)
+- [ScalaでFutureとEitherを組み合わせたときに綺麗に書く方法](https://xuwei-k.hatenablog.com/entry/20140919/1411136788)
+- [Extensible Effects in Scala](http://halcat.org/scala/extensible/index.html)
+- [A Journey into Extensible Effects in Scala](https://www.rea-group.com/blog/a-journey-into-extensible-effects-in-scala/)
+- [MonadTransformer とは何か](https://gist.github.com/gakuzzzz/ce10189cdd40427951bb5fadf18403b9)
+- [Monad Transformers for the working programmer](https://blog.buildo.io/monad-transformers-for-the-working-programmer-aa7e981190e7)