quillのio free monadがマージされてたので試してみた

この辺の事柄です。

quillにだいぶ前からIO monad入れようというissueが立っていたのですが、先日めでたくマージされたようです。 まだリリースされてないのでSNAPSHOTをビルドして試してみました。

環境などはこちら: https://github.com/matsu-chara/quill-free-example

機能とか

現状のquillだとqueryをrunした時点で副作用が発生してしまうので、うまく切り離したいというのが目的のようです。 また、クエリをtransaction内で実行させる機能やRead/Writeなどの副作用の種類を型で表現するためのEffect trackingの機能が付いています。

詳しくは GitHub - getquill/quill at edaa68bf438b0ca861d9e2bdb0893e22a8dcf70a

使い方

まず普通のバージョン

def findById(id: Long)(implicit ctx: MysqlJdbcContext[SnakeCase]): Option[Person] = {
  import ctx._
  val q = quote {
    query[Person].filter(_.id == lift(id))
  }
  run(q).headOption
}

そして、IO版。といっても runrunIO に置き換えただけです。(返り値の型が変わっているのに注目)

def findById(id: Long)(implicit ctx: MysqlJdbcContext[SnakeCase]): IO[Option[Person], Effect.Read] = {
  import ctx._
  val q = quote {
    query[Person].filter(_.id == lift(id))
  }
  runIO(q).map(_.headOption)
}

IO を呼ぶときは performIO を呼んでやればOKです。

以下のようにIOにtransactional Effectをつけてやれば、performIOを呼んだときにTransaction内で実行されるようです。 https://github.com/matsu-chara/quill-free-example/blob/2e2ad58e0970d56bfbc28bf05ecb0feb65949710/src/main/scala/matsu_chara/quill_free/Main.scala#L44

performIOの第二引数であるtransactional: Booleanをtrueにしてもtransactionで実行されるようです(内部的に Transaction() で包まれたIOがあったら transaction = true になる。)

Transactionalに実行しないと正しく動かないqueryをperformIOする場所で忘れないようにtransactionに包むのではなく、IO自体で表現できるのはうれしいですね。

Effect tracking

readmeに以下のリンクが参照されていました。 Put your writes where your master is: Compile-time restriction of Slick effect types - Daniel Westheide

ReadならSlave or Masterにクエリを向けたいけど、Writeを間違ってSlaveに向けて実行してしまうと困るので型で表現して守ろうという趣旨です。

参考:ReadWriteに対するpermissionを型で表現するといえばfujitaskなどもあります。

quillのIOにもEffectがあるので参考までに同じようなものを作ってみました。 https://github.com/matsu-chara/quill-free-example/blob/2e2ad58e0970d56bfbc28bf05ecb0feb65949710/src/main/scala/matsu_chara/quill_free/quill/RoleDb.scala

以下のようなWriteを含むクエリは roleDb の型が RoleDb[Master] なら通りますが、RoleDb[Slave]ではコンパイルエラーになります。

val personFreeRepository = new PersonFreeRepository
val ioOp = for {
  _ <- personFreeRepository.deleteAll() // Write
  _ <- personFreeRepository.insert(Person(id = 1, state = 0)) // Write
  p <- personFreeRepository.findById(1) // Read
} yield p
val personOpt = roleDb.roleBasedPerformIO(ioOp.transactional) // Read with Write with Transaction

もちろんEffect.Readだけを含むqueryならRoleDb[Slave]で実行することができます。

val ioOp = for {
  p <- personFreeRepository.findById(1) // Read
} yield p
val personOpt = roleDb.roleBasedPerformIO(ioOp) // Read

https://github.com/matsu-chara/quill-free-example/blob/2e2ad58e0970d56bfbc28bf05ecb0feb65949710/src/main/scala/matsu_chara/quill_free/Main.scala#L73-L76

WriteならMasterといった条件だけではなくReadAndWriteならTransactionalに実行しなければならない、みたいな条件まで細かく表現できるので良さそうです。

感想

好きなEffectを追加して、面白い制約(上述の記事だとExpensiveReadなどが挙げられていました。)を加えられないかなと思ったんですがsealed traitなので追加できなさそうでした。 quill/IOMonad.scala at edaa68bf438b0ca861d9e2bdb0893e22a8dcf70a · getquill/quill · GitHub

個々の機能を見ると良いんですが、インターフェースがqullの IO になってしまうので、quill依存をある程度閉じ込めて使いたい人はどうにかする必要がありそうです。

IO部分だけ切り出してすごく小さな安定したライブラリになれば依存するという判断も良さそうに感じますが、そもそも作り的にquillの色々なものに依存しているのでそのままだと色々不都合が生じそうな気がします。(まだリリースされていないSNAPSHOT版に対する感想なのでリリース版では欠点も含めて色々変わっている可能性があります。)

今後 IO 一本になる感じではなさそう?(明言はされているコメントは見当たらなかったので個人的にそういう気配を感じているだけ)ですが、しばらく様子を見ようかなと思っています。