2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-10-25

はじめに

以前にEff(Extensible Effects)でDSLを書いてみたんですが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

成果物

参考

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

前提条件

前回と同じで、「Userモデルを検索し、更新・保存するメソッドを必要としている」として、今回も環境依存を排除したDSLを構築したいと思います。

Userモデル

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

インターフェース

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

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])を受け取り、エフェクトを起こします。

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を実装したインタープリターです。これは見たまま正に実装です。

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

インタープリターを組み合わせる

この辺の定義方法は好みが分かれそうなのでお好きなように。
自分的にはこう↓した。

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自体は排除対象ではないので良しとしました。

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構築のテクニックの一つとしてこのまま載せておきます。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?