9. CONTEXT: TECH STACKCONTEXT: TECH STACK
Technologies we commonly use:
Play (web)
Anorm (persistence)
Relational database (PostgreSQL)
4
10. FOCUSING ON ANORMFOCUSING ON ANORM
How to use it (from of cial documentation)
import anorm._
def one(implicit c: java.sql.Connection): Int = {
val query: SimpleSql = SQL("select 1 as res")
query.as(SqlParser.int("res").single)
}
5
13. OUR AIMOUR AIM
What could be improved ?
Not blocking the current thread
Representing effect as type (DB read is a side-effect)
6
14. OUR AIMOUR AIM
What could be improved ?
Not blocking the current thread
Representing effect as type (DB read is a side-effect)
Doesn't compose
6
15. OUR AIMOUR AIM
What could be improved ?
Not blocking the current thread
Representing effect as type (DB read is a side-effect)
Doesn't compose
Purity (Referential Transparency + no side-effects)
6
17. An expression is called referentially transparent if it
can be replaced with its corresponding value
without changing the program's behavior.
8 . 1
19. REFERENTIAL TRANSPARENCYREFERENTIAL TRANSPARENCY
BACK TO ANORM EXAMPLEBACK TO ANORM EXAMPLE
is NOT equivalent to:
Because the database will not be called
implicit val c: Connection = ...
def one(implicit c: Connection): Int = {
val query: SimpleQuery = SQL("select 1 as res")
query.as(SqlParser.str("res").single)
}
val res = one
// console:> res: Int = 1
val res = 1 // Replacing one by its value
// console:> res: Int = 1
8 . 3
23. PRINCIPLESPRINCIPLES
Functional principles to rely on:
separating representation from interpretation
case class Plus(x: Int, y: Int)
def run(op: Plus): Int = op.x + op.y
val op = Plus(1, 2)
// console:> op: Plus = Plus(1,2)
run(op)
// console:> res0: Int = 3
11 . 2
24. PRINCIPLESPRINCIPLES
Functional principles to rely on:
separating representation from their interpretation
composition
// Composition with For-comprehensions (ie monadic)
val zipped = for {
x <- List(1, 2, 3)
y <- List("a", "b", "c")
} yield (x, y)
// console:> zipped: List[Int] = List((1, "a"), (2, "b"), (3, "c"))
11 . 3
25. PRINCIPLESPRINCIPLES
Functional principles to rely on:
separating representation from interpretation
composition
lazy evaluation
val stream = 100 #:: 200 #:: 85 #:: Stream.empty
val doubledStream = stream.map(_ * 2) // do nothing
doubledStream.print()
// console:> 200, 400, 170, empty
11 . 4
27. THE PURPOSETHE PURPOSE
Solution: Using a lazy representational structure which compose (F)
case class User(id: Int, name: String, age: Int)
def readUser(id: Int): F[User]
def updateUser(user: User): F[Unit]
// Composing DB atomic operations into a single one
def changeUserName(id: Int, newName: String): F[Unit] =
for {
user <- readUser(id)
newUser = user.copy(name = newName)
result <- updateUser(newUser)
} yield result
12 . 1
29. THE PURPOSETHE PURPOSE
Lazy representational structure => interpreter needed
val op = changeUserName(1, "John Doe")
val result: Future[Unit] = interpreter.run(op)
12 . 2
30. THE PURPOSETHE PURPOSE
Lazy representational structure => interpreter needed
Interpretation process:
val op = changeUserName(1, "John Doe")
val result: Future[Unit] = interpreter.run(op)
12 . 2
31. THE PURPOSETHE PURPOSE
Lazy representational structure => interpreter needed
Interpretation process:
1. Provide context
val op = changeUserName(1, "John Doe")
val result: Future[Unit] = interpreter.run(op)
12 . 2
32. THE PURPOSETHE PURPOSE
Lazy representational structure => interpreter needed
Interpretation process:
1. Provide context
2. Execute our operation (within our effect)
val op = changeUserName(1, "John Doe")
val result: Future[Unit] = interpreter.run(op)
12 . 2
33. THE PURPOSETHE PURPOSE
Lazy representational structure => interpreter needed
Interpretation process:
1. Provide context
2. Execute our operation (within our effect)
3. Translate it into another effect (F -> Future)
val op = changeUserName(1, "John Doe")
val result: Future[Unit] = interpreter.run(op)
12 . 2
34. SHAPING OUR SOLUTIONSHAPING OUR SOLUTION
import anorm.SimpleSql
def readUser(id: Int)(implicit c: Connection): SimpleSql[User] =
SQL(s"select * from user where id = $id")
def updateUser(user: User)(implicit c: Connection): SimpleSql[Unit] =
SQL(s"update user set ...")
// NO: doesn't compose
// def changeUserName(id: Int, newName: String): SimpleSql[Unit] = for {
// user <- readUser(id)
// newUser = user.copy(name = newUser)
// result <- updateUser(user)
// } yield result
13 . 1
35. SHAPING OUR SOLUTIONSHAPING OUR SOLUTION
def readUser(id: Int): Future[User] =
Future(SQL(s"select * from user where id = $id").as(...))
def updateUser(user: User): Future[Unit] =
Future(SQL(s"update user set ...").execute())
// commit when ???
def changeUserName(id:Int, newName:String)(implicit c:Connection): Future[Unit] =
for {
user <- readUser(id)
newUser = user.copy(name = newName)
result <- updateUser(newUser)
} yield result
13 . 2
36. SHAPING OUR SOLUTIONSHAPING OUR SOLUTION
Desiging our lazy representational structure which compose:
import java.sql.Connection
case class Query[A](f: Connection => A) {
def map[B](f: A => B): Query[B]
def flatMap[B](f: A => Query[B]): Query[B]
}
13 . 3
37. SHAPING OUR SOLUTIONSHAPING OUR SOLUTION
How could we use it ?
def one: Query[Int] = Query { implicit c =>
SQL("Select 1 as one").as(SqlParser.str("one").single)
}
13 . 4
38. ABOUT THE PRINCIPLESABOUT THE PRINCIPLES
Real word example (create a user's bank account):
case class User(id: String, name: String)
case class Account(id: String, userId: String, balance: BigDecimal)
def createUser(user: User): Query[Unit] = ???
def createAccount(account: Account, userId: String): Query[Unit] = ???
def createUserWithAccount(user: User, account: Account): Query[Unit] =
for { // composing operations
_ <- createUser(user)
_ <- createAccount(account, user.id)
} yield () // transactional
val op: Query[Unit] =
createUserWithAccount(User("1", "John Doe"), Account("a1", "1", 100))
// Query[Unit] => lazy => purity !!!
14 . 1
39. ABOUT THE PRINCIPLESABOUT THE PRINCIPLES
Real word example (create a user's bank account):
val runner = ???
runner(op) // we run the operations (caution: side-effect)
// Do it at end of your program
// Could not compose anymore
14 . 2
45. INTRODUCING READERINTRODUCING READER
Reader: monad which pass along a context
From signature: a wrapper over a function (of 1 arg)
map == function composition (g andThen f)
class Reader[-A, +B](f: A => B)
18 . 1
46. INTRODUCING READERINTRODUCING READER
Reader: monad which pass along a context
From signature: a wrapper over a function (of 1 arg)
map == function composition (g andThen f)
atMap == function composition with wrapping / unwrapping
class Reader[-A, +B](f: A => B)
18 . 1
47. INTRODUCING READERINTRODUCING READER
Implementation could be easily derived:
case class Reader[-A, +B](f: A => B) {
def map[C](g: B => C): Reader[A, C] =
Reader(g compose f) // function composition
def flatMap[C](g: B => Reader[A, C]): Reader[A, C] =
Reader(a => g(f(a)).f(a)) // composition & unwrapping
}
object Reader {
def pure[C, A](a: A): Reader[C, A] = Reader(_ => a)
// Get only the context
def ask[A]: Reader[A, A] = Reader(identity)
}
18 . 2
48. DEDUCING IMPLEMENTATIONDEDUCING IMPLEMENTATION
Query could be implemented in term of Reader:
type Query[A] = Reader[Connection, A]
object Query {
def pure[A](a: A) = Reader.pure[Connection, A](a)
def apply[A](f: Connection => A) = new Query(f)
}
19
49. HANDLING MONAD STACKHANDLING MONAD STACK
Recurrent problem with monad:
Query == Reader
How to flatMap over Reader stack ?
def divideBy2(x: Int): Query[Option[Int]]
def divideBy3(x: Int): Query[Option[Int]]
// Query is monad, Option is a monad
// but Query[Option[_]] is not
// how to compose Query[Option[_]] without nesting ?
def divideBy6(x: Int): Query[Option[Int]] = for {
y <- divideBy2(x)
z <- divideBy3(y) // DOES NOT COMPILE: divideBy3 expect Int, y is Option[Int]
} yield z
20
50. TRANSFORMERSTRANSFORMERS
One solution: Monad transformer
Transformer for Reader: ReaderT
/** @param F the monad we add to our stack */
case class ReaderT[F[_], -A, +B](f: A => F[B]) {
def map[C](g: B => C)(implicit F: Functor[F]): ReaderT[F, A, C] =
ReaderT(a => F.map(f(a))(g))
def flatMap[C](g: B => ReaderT[F, A, C])(
implicit M: Monad[F]
): ReaderT[F, A, C] =
ReaderT(a => M.flatMap(f(a))(b => g(b).f(a)))
}
21 . 1
58. GETTING CONTEXTGETTING CONTEXT
Pre-requisite: how to get the context ?
Need to abstract the way we get the connection
solution: Loan pattern
trait WithResource {
def apply[A](f: Connection => Future[A]): Future[A]
}
25
59. GETTING CONTEXTGETTING CONTEXT
Pre-requisite: how to get the context ?
Need to abstract the way we get the connection
solution: Loan pattern
User need to implement it and pass it when trying to interpret a
query
trait WithResource {
def apply[A](f: Connection => Future[A]): Future[A]
}
25
60. RESOURCE LOANER EXAMPLERESOURCE LOANER EXAMPLE
val loaner = new WithResource {
def apply[A](f: Connection => Future[A]): Future[A] = {
val conn: Connection = DriverManager.getConnection(URL, USER, PASSWD)
f(conn)
.andThen {
case Success(x) =>
connection.commit()
x
case Failure(ex) =>
connection.rollback()
}
.andThen { case _ => connection.close() }
}
}
26
64. NAIVE INTERPRETERNAIVE INTERPRETER
Naive runner implementation
Edge case: M == Future
The connection will be closed before the termination of our
operation
class QueryRunner(wr: WithResource)(implicit ec: ExecutionContext) {
def apply[M[_], T](query: QueryT[M, T]): Future[M[T]] =
wr { connection =>
Future(query.f(connection))
}
}
27
65. REFINED INTERPRETERREFINED INTERPRETER
How to handle composition with asynchronous effects ?
Solution: A typeclass which handle composition
trait ComposeWithCompletion[F[_], Out] {
type Outer
def apply[In](loaner: WithResource[In], f: In => F[Out]): Future[Outer]
}
28 . 1
66. REFINED INTERPRETERREFINED INTERPRETER
Two instances of ComposeWithCompletion:
one to compose on Future
implicit def futureOut[A] = // asynchronous effect
new ComposeWithCompletion[Future, A] {
type Outer = A
def apply[In](
loaner: WithResource[In],
f: In => Future[A]
): Future[Outer] = loaner(f)
}
28 . 2
67. REFINED INTERPRETERREFINED INTERPRETER
Two instances of ComposeWithCompletion:
one for other effects (with low priority)
implicit def pureOut[F[_], A] = // synchronous effects
new ComposeWithCompletion[F, A] {
type Outer = F[A]
def apply[In](loaner: WithResource[In], f: In => F[A]): Future[Outer] =
loaner(r => Future(f(r)))
}
28 . 3
68. REFINED INTERPRETERREFINED INTERPRETER
Two instances of ComposeWithCompletion:
one to compose on Future
one for other effects (with low priority)
trait LowPriorityCompose {
implicit def pureOut[F[_], A] = ???
}
object ComposeWithCompletion extends LowPriorityCompose {
implicit def futureOut[A] = ???
}
28 . 4
74. QUERY-MONAD LIBRARYQUERY-MONAD LIBRARY
compiled into a library:
open-source (contribution accepted ;-) )
modulable and extensible
based on cats
https://github.com/zengularity/query-
monad
34
75. FEATURES:FEATURES:
abstracted Query
type Query[Resource, A] = Reader[Resource, A]
type QueryT[F[_], Resource, A] = ReaderT[F, Resource, A]
type QueryO[Resource, A] = QueryT[Option, Resource, A]
type QueryE[Resource, Err, A] = QueryT[Either[Err, ?], Resource, A]
35
76. FEATURES:FEATURES:
aliases for sql databases (context == java.sql.Connection)
import java.sql.Connection
type SqlQuery[A] = Query[Connection, A]
type SqlQueryT[F[_], A] = QueryT[F, Connection, A]
type SqlQueryO[A] = QueryO[Connection, A]
type SqlQueryE[A, Err] = QueryE[Connection, A, Err]
// aliases for interpreter
type WithSqlConnection = WithResource[Connection]
type SqlQueryRunner = QueryRunner[Connection]
36
77. FEATURES:FEATURES:
Resource loaner implementation for Play 2
import java.sql.Connection
import play.api.db.Database
class WithPlayTransaction(db: Database) extends WithSqlConnection {
def apply[A](f: Connection => A): A = { ... }
}
37