Extensible Effects でDSLを書く 【Scala】

# 始めに
「Extensible Effects（以下Eff）」にすっかりハマったので自作Effectを利用して環境依存（DBドライバ等）を追い出すDSLを書いてみる。

Scalaに関わらず普通にDBドライバ等の環境に依存したプログラミングしていた場合、ある日突然「Aドライバではなくて非互換のBドライバを使え」とか「DBじゃなくTextに書き出せ」とか言われたら大幅な改修が出てきてしまうというのはMVCやらクリーンアーキテクチャを説明する際によく出てくる話だったりします。

Scalaの場合、そういった環境依存を排除するにはインターフェースを切り出してモナドを利用するとか、フリーモナドでDSLを構築するとか、そういった手法がよく取り上げられていると思います。

今回はそれを「 *モナドをスタックとしてシンプルに積むことができるEff* 」を使ったDSLを書いて解決してみようという話です。書き味はフリーモナドでDSLを構築するのと似ています。

**＊ Eff自体の細かな解説はしません。詳細は[ドキュメント](http://atnos-org.github.io/eff/index.html)等を確認ください。**

## 主な環境
- scala 2.12.8
- eff 5.5.0
- eff-cats-effect 5.5.0
- eff-doobie 5.5.0
- cats-core 1.1.0
- cats-effect 1.0.0-RC2
- doobie 0.6.0-M2
- h2 1.4.192

# 例えば
こんなモデルがあったとして

```scala:User.scala
final case class User(id: Int, name: String)
```

上記Userの検索と保存を期待するリポジトリと、それを利用して「Userをidで検索し、存在しなければ左に例外、存在したら右にnameを上書きしたUserを持つEitherとして返す」メソッド`def update(id: Int, name: String): Either[Throwable, User]`を求めたいとします。

# 普通に実装してみる
DBライブラリ（今回はdoobie）を使って環境依存した実装の場合、

```scala:NormalUserRepository.scala
import doobie._
import doobie.implicits._

class NormalUserRepository {

  def find(id: Int): ConnectionIO[Option[User]] =
    sql"""|SELECT
          |  id
          |  , name
          |FROM
          |  users
          |WHERE
          |  id = $id
          |""".stripMargin.query[User].option

  def save(user: User): ConnectionIO[Unit] =
    sql"""|INSERT INTO
          |  users
          |  (
          |    id
          |    , name
          |  )
          |VALUES
          |  (
          |    ${user.id}
          |    , ${user.name}
          |  )
          |ON DUPLICATE KEY UPDATE
          |  id = ${user.id}
          |  , name = ${user.name}
          |""".stripMargin.update.run.map(_ => ())
}
```

こんな感じになって、最終的なメソッドは

```scala:NormalUserManager.scala
import cats.data._
import cats.effect._
import cats.free.Free
import cats.implicits._
import doobie._
import doobie.free.connection.ConnectionOp
import doobie.implicits._

object NormalUserManager {

  val repository = new NormalUserRepository()

  val tx: Transactor[IO] = Transactor.fromDriverManager[IO](
    "org.h2.Driver",
    "jdbc:h2:mem:test;DATABASE_TO_UPPER=false;MODE=MYSQL;DB_CLOSE_DELAY=-1",
    "sa",
    ""
  )

  def update(id: Int, name: String): Either[Throwable, User] =
    (for {
      user <- EitherT[ConnectionIO, Throwable, User](repository.find(id).map {
        case Some(v) =>
          v.asRight[Throwable]
        case None =>
          new Exception("Not found").asLeft[User]
      })
      updated = user.copy(name = name)
      _ <- EitherT[ConnectionIO, Throwable, Unit](
        repository.save(updated).map(_.asRight[Throwable])
      )
    } yield updated).value.transact(tx).unsafeRunSync()
}
```

こんな感じになるのではないかと思います。

見ての通り、doobieの`ConnectionIO[_]`（`Free[ConnectionOp, A]`のエイリアス）にべったり依存しています。

「doobieやめてslickにしろ」なんて話になった場合、`update`メソッドだけの改修であれば大した工数ではありませんが、こういったメソッドが何個もあった場合は面倒なことになりそうです。


# Effを使って
ではEffを使ってDSL化してみましょう。

まずはADT（代数的データ型）から定義します。

```scala:adt.scala
sealed trait UserRepositoryOp[A]

final case class Find(id: Int) extends UserRepositoryOp[Option[User]]

final case class Save(user: User) extends UserRepositoryOp[Unit]
```

次にEffectを起こすクリエーターを用意します。

```scala:UserRepositoryCreator.scala
import org.atnos.eff._

trait UserRepositoryCreator {

  type _userRepositoryOp[R] = UserRepositoryOp |= R

  def find[R: _userRepositoryOp](id: Int): Eff[R, Option[User]] =
    EffCreation.send(Find(id))

  def save[R: _userRepositoryOp](user: User): Eff[R, Unit] =
    EffCreation.send(Save(user))
}

object UserRepositoryCreator extends UserRepositoryCreator
```

この時点でDBドライバ等の環境は`[R: _userRepositoryOp]`によって最終的に実装されるまで隠匿されます。

続いて実装時に用いるdoobie用のインターラプターを用意します。

```scala:UserRepositoryDoobieInterpretation
import doobie.implicits._
import org.atnos.eff._
import org.atnos.eff.addon.doobie.DoobieConnectionIOEffect._
import org.atnos.eff.interpret._

trait UserRepositoryDoobieInterpretation {

  private def findEffect[R: _connectionIO](id: Int) =
    for {
      queried <- fromConnectionIO(sql"""|SELECT
                                        |  id
                                        |  , name
                                        |FROM
                                        |  users
                                        |WHERE
                                        |  id = $id
                                        |""".stripMargin.query[User].option)
    } yield queried

  private def saveEffect[R: _connectionIO](user: User) =
    for {
      queried <- fromConnectionIO(
        sql"""|INSERT INTO
              |  users
              |  (
              |    id
              |    , name
              |  )
              |VALUES
              |  (
              |    ${user.id}
              |    , ${user.name}
              |  )
              |ON DUPLICATE KEY UPDATE
              |  id = ${user.id}
              |  , name = ${user.name}
              |""".stripMargin.update.run.map(_ => ())
      )
    } yield queried

  def runUserRepository[R, U, A](effects: Eff[R, A])(
      implicit member: Member.Aux[UserRepositoryOp, R, U],
      connectionIO: _connectionIO[U]
  ): Eff[U, A] =
    translate(effects)(new Translate[UserRepositoryOp, U] {
      override def apply[X](kv: UserRepositoryOp[X]): Eff[U, X] = kv match {
        case Find(id) =>
          findEffect[U](id)
        case Save(user) =>
          saveEffect[U](user)
      }
    })
}

object UserRepositoryDoobieInterpretation
    extends UserRepositoryDoobieInterpretation
```

この段階ではピンと来ないとは思いますが`runUserRepository`実行後に環境である`ConnectionIO`がスタックに積まれるイメージです。他にも依存がある場合もここで積むことが可能です。

~~また、残念なことですがインターラプターにおいては **型安全は守られなれないことに注意してください。** Effectは全て`asInstanceOf[X]`で返す必要があります。~~
**!!2019/10/26訂正!! Twitterで型安全でいけると指摘いただきました! ごめんなさい! IntelliJが解釈してくれないだけでした😢**

おまけで`runUserRepository`を既存の`runXX`同様にチェーンで呼び出せるようにしておきます。

```scala:syntax.scala
import org.atnos.eff._
import org.atnos.eff.addon.doobie.DoobieConnectionIOEffect._

trait Syntax {
  implicit final def toUserRepositoryOps[R, A](
      effects: Eff[R, A]
  ): UserRepositoryOps[R, A] = new UserRepositoryOps[R, A](effects)
}

object Syntax extends Syntax

final class UserRepositoryOps[R, A](private val effects: Eff[R, A])
    extends AnyVal {

  def runUserRepository[U](
      implicit member: Member.Aux[UserRepositoryOp, R, U],
      connectionIO: _connectionIO[U]
  ): Eff[U, A] = UserRepositoryDoobieInterpretation.runUserRepository(effects)
}
```

割と手間がかかりましたが、以上でDSLの準備は完了です。

最終的にDSLを利用する`update`メソッドを実装します。

```scala:EffUserManager.scala
import cats.effect._
import cats.implicits._
import doobie._
import org.atnos.eff._
import org.atnos.eff.all._
import org.atnos.eff.syntax.addon.cats.effect._
import org.atnos.eff.syntax.addon.doobie._
import org.atnos.eff.syntax.all._

import com.example.Syntax._

object EffUserManager {

  private val tx = Transactor.fromDriverManager[IO](
    "com.mysql.cj.jdbc.Driver",
    "jdbc:mysql://localhost:3306/db?useSSL=false",
    "root",
    "password"
  )

  def update(id: Int, name: String): Either[Throwable, User] = {
    type Stack = Fx.fx4[UserRepositoryOp, ThrowableEither, ConnectionIO, IO]

    for {
      optionalUser <- UserRepositoryCreator.find[Stack](id)
      user <- fromEither[Stack, Throwable, User](optionalUser match {
        case Some(v) =>
          v.asRight[Throwable]
        case None =>
          new Exception("Not found").asLeft[User]
      })
      updated = user.copy(name = name)
      _ <- UserRepositoryCreator.save[Stack](updated)
    } yield updated).runUserRepository
      .runConnectionIO(tx)
      .runEither[Throwable]
      .unsafeRunSync
  }
}
```

`Stack`に`IO`が含まれていますが、これは`runConnectionIO(_)`が`IO`を積むためです。

本筋とは異なりますがEffの特徴である`for-comprehension`の綺麗さが出ているのが何となく分かってもらえるでしょうか？

ともあれ後は「`UserRepositorySlickInterpretation`」とか「`UserRepositoryTextInterpretation`」とか必要となった環境のインターラプターを用意してやるだけで、`update`メソッド側では`Stack`と`runXX`の差し替えのみで環境を切り替えることが可能となります。

# 終わりに
以上、EffでDSLを書いてみるでした。

- ボイラープレートが多い？
- importが多い？
- `runXX`が煩わしい？
- 型を明示しないとIDEが重い（IntelliJお前だよ）？

そこはトレードオフというやつです。Effは戻り値の方が`Eff[R, *]`となるため、モナドトランスフォーマー等による型合わせからは解放されます。これは呼び出しが深くなればなるほど効果を発揮します（それに伴ってスタックや`context bound`が増えてしまうというこれまたトレードオフなデメリットを持っていますが・・・）。

自分のようにモナドトランスフォーマーがしんどいと思われる方は使ってみてはいかがでしょうか？