Tagless Final と Eff を組み合わせてみる【Scala】

# はじめに

[以前にEff(Extensible Effects)でDSLを書いてみたんですが](https://qiita.com/lightstaff/items/82173d23589c65094a9e) 、~~ADT（代数的データ型）を利用するとインタープリターの戻り値に`asInstanceOf[]`を使う必要があり型安全ではなくなるという弱点がありました。~~
**!!2019/10/26訂正!! Twitterで型安全でいけると指摘いただきました! ごめんなさい! IntelliJが解釈してくれないだけでした😢**

そこでADTを必要としない構築方法である「Tagless Final」を用いて型安全かつEffの利点を利用できるDSLを書いてみたいと思います。

**※Tagless FinalやEffの詳細な解説はしません**

## 環境

- eff 5.5.2
- eff-cats-effect 5.5.2
- eff-doobie 5.5.2
- cats-core 2.0.0
- cats-effect 2.0.0
- doobbie-core 0.8.0-RC1
- h2 1.4.200

## 成果物

https://github.com/lightstaff/tagless-final-with-eff

## 参考
[Interpreting Tagless Final DSLs with Eff] (https://www.becompany.ch/en/blog/2018/09/27/tagless-final-and-eff)

# 前提条件
[前回](https://qiita.com/lightstaff/items/82173d23589c65094a9e)と同じで、「Userモデルを検索し、更新・保存するメソッドを必要としている」として、今回も環境依存を排除したDSLを構築したいと思います。

Userモデル

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

# インターフェース
Tagless Finalでは高カインド型を用いたインターフェースでDSLを表現するようです。

```scala:UserRepository.scala
trait UserRepository[F[_]] {

  // 検索
  def find(id: Int): F[Option[User]]

  // 保存
  def save(user: User): F[Unit]
}
```

# Effインタープリター
Eff用のインタープリターです。これはあくまで上っ面であり、環境依存（Effで言うとこのStack）を積んだインタープリター（引数の`interpreter: UserRepository[F]`）を受け取り、エフェクトを起こします。

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

trait UserRepositoryEffInterpreter {

  // Eff用インタープリター
  // F型はR型の「Member」
  def effInterpreter[R, F[_]](
      interpreter: UserRepository[F]
  )(implicit evidence: F |= R): UserRepository[Eff[R, *]] =
    new UserRepository[Eff[R, *]] {

      override def find(id: Int): Eff[R, Option[User]] =
        Eff.send(interpreter.find(id))

      override def save(user: User): Eff[R, Unit] =
        Eff.send(interpreter.save(user))
    }
}

object UserRepositoryEffInterpreter extends UserRepositoryEffInterpreter
```

# ConnectionIOインタープリター
環境である`doobie.ConnectionIO`を実装したインタープリターです。これは見たまま正に実装です。

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

trait UserRepositoryConnectionIOInterpreter {

  // ConnectionIO用インタープリター
  val connectionIOInterpreter: UserRepository[ConnectionIO] =
    new UserRepository[ConnectionIO] {

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

      override 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(_ => ()) // Int to Unit
    }
}

object UserRepositoryConnectionIOInterpreter
    extends UserRepositoryConnectionIOInterpreter
```

# インタープリターを組み合わせる
この辺の定義方法は好みが分かれそうなのでお好きなように。
自分的にはこう↓した。

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

trait Syntax {

  implicit final def toEffConnectionIOInterpreter[R: _connectionIO]
      : UserRepository[Eff[R, *]] =
    UserRepositoryEffInterpreter.effInterpreter[R, ConnectionIO](
      UserRepositoryConnectionIOInterpreter.connectionIOInterpreter
    )
}

object Syntax extends Syntax
```

# Main
今回はH2にデータを用意することにしたので前半部はその準備です。
本来のTagless Finalであればupdateメソッドは`def update[F: Monad](id: Int, name: String)(implicit userRepository: UserRepository[F])`としたいところですが、Effの積み上げを利用したかったので`Eff[R, *]`と決め打ちしています。今回Eff自体は排除対象ではないので良しとしました。

```scala:Main.scala
import scala.concurrent.ExecutionContext

import cats.effect._
import cats.implicits._
import doobie._
import doobie.implicits._
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._

object Main extends App {

  import Syntax._ // 必須

  implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)

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

  // テーブル定義
  val create =
    sql"""|CREATE TABLE users
          |(
          |  id INT NOT NULL PRIMARY KEY
          |  , name VARCHAR(50)
          |)
          |""".stripMargin.update.run

  // User(1, ""）を追加
  val insert =
    sql"""INSERT INTO users
         |(
         |  id
         |  , name
         |)
         |VALUES
         |(
         |  1,
         |  ''
         |)
         |""".stripMargin.update.run

  // 上記2つを実行
  (create, insert).mapN(_ + _).transact(tx).unsafeRunSync()

  // 欲しかったメソッド
  // ThrowableEitherを積み上げているけどConnectionIOはまだ出てきていない
  def update[R: _throwableEither](id: Int, name: String)(
      implicit userRepository: UserRepository[Eff[R, *]]
  ): Eff[R, User] =
    for {
      optionalUser <- userRepository.find(id)
      user <- fromEither(
               optionalUser.toRight[Throwable](new Exception("Not Found"))
             )
      updated = user.copy(name = name)
      _ <- userRepository.save(updated)
    } yield updated

  // EffのStack
  // ConnectionIOはIOを積み上げる
  type Stack = Fx.fx3[ThrowableEither, ConnectionIO, IO]

  // 実行
  // ここでSyntax.toEffConnectionIOInterpreter[R: _connectionIO]が解決され、
  // ConnectionIOが積み上がる
  val result = update[Stack](1, "test")
    .runEither[Throwable]
    .runConnectionIO(tx)
    .unsafeRunSync

  println(result)  // Right(User(1, test))
}
```

# おわりに
ボイラープレート（？）量はあまり変わらないかも。インタープリターを多重化してるところがミソではあるけども、それがトリッキーである気もする。
とりあえず~~「Eff + ADT」の型安全では無かったDSLが~~「Tagless Final + Eff」で型安全なDSLになりましたとさ。

## 2019/10/26 追記
「Eff + ADT」が型安全なことが判明したので当初の目的がなくなってしまいましたが、DSL構築のテクニックの一つとしてこのまま載せておきます。