この辺の事柄です。
- IO monad · Issue #21 · getquill/quill · GitHub
- io free monad by fwbrasil · Pull Request #881 · getquill/quill · GitHub
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版。といっても run
を runIO
に置き換えただけです。(返り値の型が変わっているのに注目)
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
WriteならMasterといった条件だけではなくReadAndWriteならTransactionalに実行しなければならない、みたいな条件まで細かく表現できるので良さそうです。
感想
好きなEffectを追加して、面白い制約(上述の記事だとExpensiveRead
などが挙げられていました。)を加えられないかなと思ったんですがsealed traitなので追加できなさそうでした。
quill/IOMonad.scala at edaa68bf438b0ca861d9e2bdb0893e22a8dcf70a · getquill/quill · GitHub
個々の機能を見ると良いんですが、インターフェースがqullの IO
になってしまうので、quill依存をある程度閉じ込めて使いたい人はどうにかする必要がありそうです。
IO部分だけ切り出してすごく小さな安定したライブラリになれば依存するという判断も良さそうに感じますが、そもそも作り的にquillの色々なものに依存しているのでそのままだと色々不都合が生じそうな気がします。(まだリリースされていないSNAPSHOT版に対する感想なのでリリース版では欠点も含めて色々変わっている可能性があります。)
今後 IO
一本になる感じではなさそう?(明言はされているコメントは見当たらなかったので個人的にそういう気配を感じているだけ)ですが、しばらく様子を見ようかなと思っています。