Skip to content

Commit

Permalink
Merge pull request #881 from getquill/io-monad
Browse files Browse the repository at this point in the history
io free monad
  • Loading branch information
fwbrasil authored Sep 15, 2017
2 parents 04f12fe + a9c7a6e commit edaa68b
Show file tree
Hide file tree
Showing 26 changed files with 1,051 additions and 54 deletions.
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,84 @@ val a = quote {
ctx.run(a)
// DELETE FROM Person WHERE name = ''
```

## IO Monad

Quill provides an IO monad that allows the user to express multiple computations and execute them separately. This mechanism is also known as a free monad, which provides a way of expressing computations as referentially-transparent values and isolates the unsafe IO operations into a single operation. For instance:

```
// this code using Future
val p = Person(0, "John", 22)
ctx.run(query[Person].insert(lift(p))).flatMap { _ =>
ctx.run(query[Person])
}
// isn't referentially transparent because if you refactor the second database
// interaction into a value, the result will be different:
val allPeople = ctx.run(query[Person])
ctx.run(query[Person].insert(lift(p))).flatMap { _ =>
allPeople
}
// this happens because `ctx.run` executes the side-effect (database IO) immediately
```

```scala
// The IO monad doesn't perform IO immediately, so both computations:

val p = Person(0, "John", 22)

val a =
ctx.runIO(query[Person].insert(lift(p))).flatMap { _ =>
ctx.runIO(query[Person])
}


val allPeople = ctx.runIO(query[Person])

val b =
ctx.runIO(query[Person].insert(lift(p))).flatMap { _ =>
allPeople
}

// produce the same result when executed

performIO(a) == performIO(b)
```

The IO monad has an interface similar to `Future`; please refer to [the class](https://github.com/getquill/quill/master/quill-core/src/main/scala/io/getquill/monad/IOMonad.scala#L38) for more information regarding the available operations.

The return type of `performIO` varies according to the context. For instance, async contexts return `Future`s while JDBC returns values synchronously.

***NOTE***: Avoid using the variable name `io` since it conflicts with Quill's package `io.getquill`.

### IO Monad and transactions

`IO` also provides the `transactional` method that delimits a transaction:

```scala
val a =
ctx.runIO(query[Person].insert(lift(p))).flatMap { _ =>
ctx.runIO(query[Person])
}

performIO(a.transactional) // note: transactional can be used outside of `performIO`
```

### Effect tracking

The IO monad tracks the effects that a computation performs in its second type parameter:

```scala
val a: IO[ctx.RunQueryResult[Person], Effect.Write with Effect.Read] =
ctx.runIO(query[Person].insert(lift(p))).flatMap { _ =>
ctx.runIO(query[Person])
}
```

This mechanism is useful to limit the kind of operations that can be performed. See this [blog post](http://danielwestheide.com/blog/2015/06/28/put-your-writes-where-your-master-is-compile-time-restriction-of-slick-effect-types.html) as an example.

## Implicit query

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def updateWebsiteTag =

lazy val commonSettings = ReleasePlugin.extraReleaseCommands ++ Seq(
organization := "io.getquill",
scalaVersion := "2.11.11",
scalaVersion := "2.12.3",
crossScalaVersions := Seq("2.11.11","2.12.3"),
libraryDependencies ++= Seq(
"org.scalamacros" %% "resetallattrs" % "1.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,26 @@ import io.getquill.context.sql.SqlContext
import io.getquill.context.sql.idiom.SqlIdiom
import io.getquill.NamingStrategy
import io.getquill.util.ContextLogger
import io.getquill.monad.ScalaFutureIOMonad

abstract class AsyncContext[D <: SqlIdiom, N <: NamingStrategy, C <: Connection](pool: PartitionedConnectionPool[C])
extends SqlContext[D, N]
with Decoders
with Encoders {
with Encoders
with ScalaFutureIOMonad {

private val logger = ContextLogger(classOf[AsyncContext[_, _, _]])

override type PrepareRow = List[Any]
override type ResultRow = RowData

override type RunQueryResult[T] = Future[List[T]]
override type RunQuerySingleResult[T] = Future[T]
override type RunActionResult = Future[Long]
override type RunActionReturningResult[T] = Future[T]
override type RunBatchActionResult = Future[List[Long]]
override type RunBatchActionReturningResult[T] = Future[List[T]]
override type Result[T] = Future[T]
override type RunQueryResult[T] = List[T]
override type RunQuerySingleResult[T] = T
override type RunActionResult = Long
override type RunActionReturningResult[T] = T
override type RunBatchActionResult = List[Long]
override type RunBatchActionReturningResult[T] = List[T]

override def close = {
Await.result(pool.close, Duration.Inf)
Expand All @@ -56,6 +59,12 @@ abstract class AsyncContext[D <: SqlIdiom, N <: NamingStrategy, C <: Connection]
f(TransactionalExecutionContext(ec, c))
}

override def performIO[T](io: IO[T, _], transactional: Boolean = false)(implicit ec: ExecutionContext): Result[T] =
transactional match {
case false => super.performIO(io)
case true => transaction(super.performIO(io)(_))
}

def executeQuery[T](sql: String, prepare: Prepare = identityPrepare, extractor: Extractor[T] = identityExtractor)(implicit ec: ExecutionContext): Future[List[T]] = {
val (params, values) = prepare(Nil)
logger.logQuery(sql, params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,32 @@ import com.typesafe.config.Config
import scala.collection.JavaConverters._
import io.getquill.context.cassandra.CassandraSessionContext
import com.datastax.driver.core.Cluster
import io.getquill.monad.ScalaFutureIOMonad

class CassandraAsyncContext[N <: NamingStrategy](
cluster: Cluster,
keyspace: String,
preparedStatementCacheSize: Long
)
extends CassandraSessionContext[N](cluster, keyspace, preparedStatementCacheSize) {
extends CassandraSessionContext[N](cluster, keyspace, preparedStatementCacheSize)
with ScalaFutureIOMonad {

def this(config: CassandraContextConfig) = this(config.cluster, config.keyspace, config.preparedStatementCacheSize)
def this(config: Config) = this(CassandraContextConfig(config))
def this(configPrefix: String) = this(LoadConfig(configPrefix))

private val logger = ContextLogger(classOf[CassandraAsyncContext[_]])

override type RunQueryResult[T] = Future[List[T]]
override type RunQuerySingleResult[T] = Future[T]
override type RunActionResult = Future[Unit]
override type RunBatchActionResult = Future[Unit]
override type Result[T] = Future[T]
override type RunQueryResult[T] = List[T]
override type RunQuerySingleResult[T] = T
override type RunActionResult = Unit
override type RunBatchActionResult = Unit

override def performIO[T](io: IO[T, _], transactional: Boolean = false)(implicit ec: ExecutionContext): Result[T] = {
if (transactional) logger.underlying.warn("Cassandra doesn't support transactions, ignoring `io.transactional`")
super.performIO(io)
}

def executeQuery[T](cql: String, prepare: Prepare = identityPrepare, extractor: Extractor[T] = identityExtractor)(implicit ec: ExecutionContext): Future[List[T]] = {
val (params, bs) = prepare(super.prepare(cql))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ class CassandraStreamContext[N <: NamingStrategy](

private val logger = ContextLogger(classOf[CassandraStreamContext[_]])

override type RunQueryResult[T] = Observable[T]
override type RunQuerySingleResult[T] = Observable[T]
override type RunActionResult = Observable[Unit]
override type RunBatchActionResult = Observable[Unit]
override type Result[T] = Observable[T]
override type RunQueryResult[T] = T
override type RunQuerySingleResult[T] = T
override type RunActionResult = Unit
override type RunBatchActionResult = Unit

protected def page(rs: ResultSet): Task[Iterable[Row]] = Task.defer {
val available = rs.getAvailableWithoutFetching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,33 @@ import io.getquill.util.{ ContextLogger, LoadConfig }
import io.getquill.context.cassandra.CassandraSessionContext
import scala.collection.JavaConverters._
import com.datastax.driver.core.Cluster
import io.getquill.monad.SyncIOMonad

class CassandraSyncContext[N <: NamingStrategy](
cluster: Cluster,
keyspace: String,
preparedStatementCacheSize: Long
)
extends CassandraSessionContext[N](cluster, keyspace, preparedStatementCacheSize) {
extends CassandraSessionContext[N](cluster, keyspace, preparedStatementCacheSize)
with SyncIOMonad {

def this(config: CassandraContextConfig) = this(config.cluster, config.keyspace, config.preparedStatementCacheSize)
def this(config: Config) = this(CassandraContextConfig(config))
def this(configPrefix: String) = this(LoadConfig(configPrefix))

private val logger = ContextLogger(classOf[CassandraSyncContext[_]])

override type Result[T] = T
override type RunQueryResult[T] = List[T]
override type RunQuerySingleResult[T] = T
override type RunActionResult = Unit
override type RunBatchActionResult = Unit

override def performIO[T](io: IO[T, _], transactional: Boolean = false): Result[T] = {
if (transactional) logger.underlying.warn("Cassandra doesn't support transactions, ignoring `io.transactional`")
super.performIO(io)
}

def executeQuery[T](cql: String, prepare: Prepare = identityPrepare, extractor: Extractor[T] = identityExtractor): List[T] = {
val (params, bs) = prepare(super.prepare(cql))
logger.logQuery(cql, params)
Expand Down
7 changes: 5 additions & 2 deletions quill-core/src/main/scala/io/getquill/MirrorContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ package io.getquill
import io.getquill.context.Context
import io.getquill.context.mirror.{ MirrorDecoders, MirrorEncoders, Row }
import io.getquill.idiom.{ Idiom => BaseIdiom }

import scala.util.{ Failure, Success, Try }
import io.getquill.monad.SyncIOMonad

class MirrorContextWithQueryProbing[Idiom <: BaseIdiom, Naming <: NamingStrategy]
extends MirrorContext[Idiom, Naming] with QueryProbing

class MirrorContext[Idiom <: BaseIdiom, Naming <: NamingStrategy]
extends Context[Idiom, Naming]
with MirrorEncoders
with MirrorDecoders {
with MirrorDecoders
with SyncIOMonad {

override type PrepareRow = Row
override type ResultRow = Row

override type Result[T] = T
override type RunQueryResult[T] = QueryMirror[T]
override type RunQuerySingleResult[T] = QueryMirror[T]
override type RunActionResult = ActionMirror
Expand Down
13 changes: 7 additions & 6 deletions quill-core/src/main/scala/io/getquill/context/Context.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ trait Context[Idiom <: io.getquill.idiom.Idiom, Naming <: NamingStrategy]
extends Closeable
with CoreDsl {

type Result[T]
type RunQuerySingleResult[T]
type RunQueryResult[T]
type RunActionResult
Expand All @@ -26,12 +27,12 @@ trait Context[Idiom <: io.getquill.idiom.Idiom, Naming <: NamingStrategy]

def probe(statement: String): Try[_]

def run[T](quoted: Quoted[T]): RunQuerySingleResult[T] = macro QueryMacro.runQuerySingle[T]
def run[T](quoted: Quoted[Query[T]]): RunQueryResult[T] = macro QueryMacro.runQuery[T]
def run(quoted: Quoted[Action[_]]): RunActionResult = macro ActionMacro.runAction
def run[T](quoted: Quoted[ActionReturning[_, T]]): RunActionReturningResult[T] = macro ActionMacro.runActionReturning[T]
def run(quoted: Quoted[BatchAction[Action[_]]]): RunBatchActionResult = macro ActionMacro.runBatchAction
def run[T](quoted: Quoted[BatchAction[ActionReturning[_, T]]]): RunBatchActionReturningResult[T] = macro ActionMacro.runBatchActionReturning[T]
def run[T](quoted: Quoted[T]): Result[RunQuerySingleResult[T]] = macro QueryMacro.runQuerySingle[T]
def run[T](quoted: Quoted[Query[T]]): Result[RunQueryResult[T]] = macro QueryMacro.runQuery[T]
def run(quoted: Quoted[Action[_]]): Result[RunActionResult] = macro ActionMacro.runAction
def run[T](quoted: Quoted[ActionReturning[_, T]]): Result[RunActionReturningResult[T]] = macro ActionMacro.runActionReturning[T]
def run(quoted: Quoted[BatchAction[Action[_]]]): Result[RunBatchActionResult] = macro ActionMacro.runBatchAction
def run[T](quoted: Quoted[BatchAction[ActionReturning[_, T]]]): Result[RunBatchActionReturningResult[T]] = macro ActionMacro.runBatchActionReturning[T]

protected val identityPrepare: Prepare = (Nil, _)
protected val identityExtractor = identity[ResultRow] _
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package io.getquill.context.mirror
import java.time.LocalDate
import java.util.{ Date, UUID }

import io.getquill.MirrorContext

import scala.reflect.ClassTag
import io.getquill.context.Context

trait MirrorDecoders {
this: MirrorContext[_, _] =>
this: Context[_, _] =>

override type PrepareRow = Row
override type ResultRow = Row
override type Decoder[T] = MirrorDecoder[T]

case class MirrorDecoder[T](decoder: BaseDecoder[T]) extends BaseDecoder[T] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package io.getquill.context.mirror
import java.time.LocalDate
import java.util.{ Date, UUID }

import io.getquill.MirrorContext
import java.util.Date
import io.getquill.context.Context

trait MirrorEncoders {
this: MirrorContext[_, _] =>
this: Context[_, _] =>

override type PrepareRow = Row
override type ResultRow = Row
override type Encoder[T] = MirrorEncoder[T]

case class MirrorEncoder[T](encoder: BaseEncoder[T]) extends BaseEncoder[T] {
Expand Down
Loading

0 comments on commit edaa68b

Please sign in to comment.