Doobie integration

It’s easy to embed a doobie program in a Freestyle program using the frees-doobie module. This module provides a very simple algebra with only one operation: transact. Handlers for this algebra are available if there is a doobie Transactor for the target type.

The standard freestyle and cats imports:

import freestyle.free._
import freestyle.free.implicits._

import cats.implicits._

And imports for the frees-doobie module and doobie itself:

import freestyle.free.doobie._
import freestyle.free.doobie.implicits._

import _root_.doobie._
import _root_.doobie.implicits._
import _root_.doobie.h2._

We will ask the database for a Person. To keep this example concise, we just use a simple query and don’t bother to create actual tables:

case class Person(name: String, birthYear: Int)
// defined class Person

val getPerson: ConnectionIO[Person] = sql"SELECT 'Alonzo Church', 1903".query[Person].unique
// getPerson: doobie.ConnectionIO[Person] = Free(...)

We can embed this doobie ConnectionIO program in a freestyle program. We start with the most trivial case by only using the DoobieM algebra:

val doobieFrees: FreeS[DoobieM.Op, Person] =
  DoobieM[DoobieM.Op].transact(getPerson)
// doobieFrees: freestyle.free.FreeS[freestyle.free.doobie.DoobieM.Op,Person] = Free(...)

To execute this FreeS program, we need a doobie Transactor for our target type; in this example we have chosen cats.effect.IO:

import cats.effect.IO
// import cats.effect.IO

implicit val xa: Transactor[IO] =
  H2Transactor[IO]("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", "").unsafeRunSync
// <console>:38: warning: method apply in object H2Transactor is deprecated (since doobie 0.5.0): This method has been renamed `newH2Transactor` to help clarify usage
//          H2Transactor[IO]("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", "").unsafeRunSync
//          ^
// xa: doobie.Transactor[cats.effect.IO] = doobie.util.transactor$Transactor$$anon$10@45bfac5e

val io = doobieFrees.interpret[IO]
// io: cats.effect.IO[Person] = IO$145682794

To check if we actually get Alonzo Church as a Person, we can use IO#unsafeRunSync in this example:

io.unsafeRunSync
// res2: Person = Person(Alonzo Church,1903)

DoobieM in a module

Only using DoobieM is not exactly useful however, as it just adds an extra level of indirection on top of ConnectionIO in this case. As a more realistic example, we will use DoobieM in a module together with another algebra:

@free trait Calc {
  def subtract(a: Int, b: Int): FS[Int]
}
// defined trait Calc
// defined object Calc

@module trait Example {
  val doobieM: DoobieM
  val calc: Calc
}
// defined trait Example
// defined object Example

A Handler[IO] for Calc:

implicit val calcHandler: Calc.Handler[IO] =
  new Calc.Handler[IO] {
    def subtract(a: Int, b: Int): IO[Int] = IO.pure(a - b)
  }
// calcHandler: Calc.Handler[cats.effect.IO] = $anon$1@1c03ee66

A program using the Example module:

def example[F[_]: DoobieM](implicit example: Example[F]): FreeS[F, (Person, Int)] =
  for {
    person <- getPerson.liftFS[F]  // analogous to example.doobieM.transact(getPerson)
    age    <- example.calc.subtract(2017, person.birthYear)
  } yield (person, age)
// example: [F[_]](implicit evidence$1: freestyle.free.doobie.DoobieM[F], implicit example: Example[F])freestyle.free.FreeS[F,(Person, Int)]

We can use Example.Op as the functor and translate the resulting program to IO:

val io2 = example[Example.Op].interpret[IO]
// io2: cats.effect.IO[(Person, Int)] = IO$580317478

io2.unsafeRunSync
// res3: (Person, Int) = (Person(Alonzo Church,1903),114)