Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

io free monad #881

Merged
merged 1 commit into from
Sep 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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