Last active
November 19, 2015 15:28
-
-
Save 1ambda/c097244d06aa54cb161d to your computer and use it in GitHub Desktop.
implementation - Programs as Values: JDBC Programming with Doobie
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use Scalaz 7.1.x to compile this script