Skip to content

Instantly share code, notes, and snippets.

@1ambda
Last active November 19, 2015 15:28
Show Gist options
  • Save 1ambda/c097244d06aa54cb161d to your computer and use it in GitHub Desktop.
Save 1ambda/c097244d06aa54cb161d to your computer and use it in GitHub Desktop.
implementation - Programs as Values: JDBC Programming with Doobie
package free
import scalaz._, Scalaz._
import scalaz.effect._
/**
* Programs as Values: JDBC Programming with Doobie
*
* video - https://www.youtube.com/watch?v=M5MF6M7FHPo
* slide - http://tpolecat.github.io/assets/sbtb-slides.pdf
*/
trait ResultSet {
def next: Boolean
def getString(index: Int): String
def getInt(index: Int): Int
def close: Unit
}
case class Person(name: String, age: Int)
/**
* def getPerson(rs: ResultSet): Person = {
* val name = rs.getString(1)
* val age = rs.getInt(2)
*
* Person(name, age)
* }
*
* - `rs` : managed resource
* - side-effect (getString, getInt)
*/
sealed trait ResultSetOp[A]
final case object Next extends ResultSetOp[Boolean]
final case class GetString(index: Int) extends ResultSetOp[String]
final case class GetInt(index: Int) extends ResultSetOp[Int]
final case object Close extends ResultSetOp[Unit]
/** if we had Monad[ResultSetOp]
* val getPerson: ResultSetOp[Person] = for {
* name <- GetString(1)
* age <- GetInt(2)
* } yield Person(name, age)
*
*
* Free[F[_], ?] is a monad for any functor `F`
* Coyoneda[S[_], ?] is a functor for any `S` at all
*
*
* By substitution, `Free[Coyoneda[S[_], ?], ?]` is a monad for any `S` at all!
*
* type FreeC[S[_], A] = Free[({type λ[α] = Coyoneda[S, α]})#λ, A]
*
* So, FreeC[ResultSetOp[_], ?] is a monad
*/
object JDBC {
import Free._, Coyoneda._
/**
* scalaz 7.2.x doesn't have FreeC so that use just Free
* SO: http://stackoverflow.com/questions/33792968/why-free-is-not-monad-instance-in-scalaz-7-1-5
*/
type CoyoResultSetOp[A] = Coyoneda[ResultSetOp, A]
type ResultSetIO[A] = Free[CoyoResultSetOp, A]
val next : ResultSetIO[Boolean] = liftFC(Next)
def getString(index: Int): ResultSetIO[String] = liftFC(GetString(index))
def getInt(index: Int) : ResultSetIO[Int] = liftFC(GetInt(index))
def close : ResultSetIO[Unit] = liftFC(Close)
def getPerson: ResultSetIO[Person] = for {
name <- getString(1)
age <- getInt(2)
} yield Person(name, age)
def getPerson1: ResultSetIO[Person] =
(getString(1) |@| getInt(2)) { Person(_, _)}
def getNextPerson: ResultSetIO[Person] =
next *> getPerson
def getPeople(n: Int): ResultSetIO[List[Person]] =
getNextPerson.replicateM(n) // List.fill(n)(getNextPerson).sequence
def getPersonOpt: ResultSetIO[Option[Person]] =
next >>= {
case true => getPerson.map(_.some)
case false => none.point[ResultSetIO]
}
def getAllPeople: ResultSetIO[Vector[Person]] =
getPerson.whileM[Vector](next)
/**
* To run out program, we should interpret it into some target monad of our choice
* using natural transformation `ResultSetOp ~> M`
*/
private def interpret(rs: ResultSet) = new (ResultSetOp ~> IO) {
def apply[A](fa: ResultSetOp[A]): IO[A] = fa match {
case Next => IO(rs.next)
case GetString(i) => IO(rs.getString(i))
case GetInt(i) => IO(rs.getInt(i))
case Close => IO(rs.close)
}
}
def run[A](a: ResultSetIO[A], rs: ResultSet): IO[A] =
Free.runFC(a)(interpret(rs))
}
package free
import java.util.concurrent.atomic.AtomicInteger
import org.scalatest.{Matchers, FunSuite}
import scalaz._, Scalaz._
class JDBCSpec extends FunSuite with Matchers {
import JDBC._
test("JDBC.getPerson") {
val rs = createDummyResultSet(5)
val r = JDBC.run(getPerson, rs).unsafePerformIO()
r.age shouldBe 0
}
test("JDBC.getNextPerson") {
val rs = createDummyResultSet(5)
// def getNextPerson = next *> getPerson
val r = JDBC.run(getNextPerson, rs).unsafePerformIO()
r.age shouldBe 1
}
test("JDBC.getAllPeople") {
val rs = createDummyResultSet(5)
val r = JDBC.run(getAllPeople, rs).unsafePerformIO()
r.length shouldBe 5
}
def createDummyResultSet(max: Int): ResultSet = new ResultSet {
val counter = new AtomicInteger(0)
val rand = new java.util.Random()
override def next: Boolean =
if (counter.getAndIncrement() < max) true
else false
override def close: Unit = {}
override def getInt(index: Int): Int =
counter.get()
override def getString(index: Int): String =
List.fill(5)(genRandomLetter).mkString
private def genRandomLetter(): Char = {
(rand.nextInt(27) + 64).toChar
}
}
}
@1ambda
Copy link
Author

1ambda commented Nov 19, 2015

Use Scalaz 7.1.x to compile this script

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment