by Yuriy Polyulya / @polyulya / +blog
Chess game player score update method. For player with: id, name, last name, games and score fields stored in MongoDB.
case class User(
id : String,
name : String,
lastName : String,
games : Long,
score : Double)
{ "_id" : "yuriy_polyulya@epam.com",
"name" : "Yuriy",
"last-name" : "Polyulya",
"games" : 15,
"score" : 100.0 }
def updateScore(id : String, gameScore : Double): Option[DBObject] = {
val users = MongoClient("localhost")("chess")("users")
val q = MongoDBObject(ID -> id)
users.findOne(q) match {
case Some(dbo) =>
val games = dbo.getAs[Long]("games")
val score = dbo.getAs[Double]("score")
(games, score) match {
case (Some(g), Some(s)) =>
val gamesU = g + 1
val scoreU = s + gameScore
val update = $set("games" -> gamesU, "score" -> scoreU)
users.update(q, update)
users.findOne(q)
case _ => None
}
case None => None
}
}
val users = MongoClient("localhost")("chess")("users")
... match {
case Some(dbo) =>
... match {
case (Some(g), Some(s)) =>
...
case _ => None
}
case None => None
}
object ChessMongoCollections {
lazy val users = MongoClient("localhost")("chess")("users") // or def
}
...
val users = ChessMongoCollections.users
def updateScore(
id : String,
gameScore : Double,
users : MongoCollection): Option[DBObject] = {
...
}
def updateScore(id : String, setScore : Double): Collection => Option[DBObject] =
users => {
val q = MongoDBObject(ID -> id)
users.findOne(q) match {
case Some(dbo) =>
val games = dbo.getAs[Long]("games")
val score = dbo.getAs[Double]("score")
(games, score) match {
case (Some(g), Some(s)) =>
val gamesU = g + 1
val scoreU = s + newScore
val update = $set("games" -> gamesU, "score" -> scoreU)
users.update(q, update)
users.findOne(q)
case _ => None
}
case None => None
}
}
Reader - for computations which read values from a shared environment.
case class DB[R](read : MongoCollection => R) {
def apply(c : MongoCollection): R = read(c)
}
MongoCollection class doesn't represent data inside it:
val collection = MongoClient("localhost")("chess")("games")
// substitute "games" collection instead of "users" collection!!!
updateScore("a@epam.com", 2.0)(collection)
Use "Tagged type" to mark MongoCollection:
type Tagged[U] = { type Tag = U }
type @@[T, U] = T with Tagged[U]
def withTag[T](c: MongoCollection) = c.asInstanceOf[MongoCollection @@ T]
type #>[Tag, R] = MongoCollection @@ Tag => R
And DB reader:
case class DB[CTag, R](read : Tag #> R) {
def apply(c : MongoCollection @@ CTag): R = read(c)
}
Extend DB Reader for: pass values from function to function, and execute sub-computations in a modified environment.
case class DB[CTag, R](read : CTag #> R) {
...
def map[B](f : R => B): DB[CTag, B] = DB { read andThen f }
}
case class DB[CTag, R](read : CTag #> R) {
...
def flatMap[B](f : R => DB[CTag, B]): DB[CTag, B] =
DB { c => (read andThen f)(c).read(c) }
}
case class DB[CTag, R](read : CTag #> R) {
def apply(c : MongoCollection @@ CTag): R = read(c)
def map[B](f : R => B): DB[CTag, B] = DB { read andThen f }
def flatMap[B](f : R => DB[CTag, B]): DB[CTag, B] =
DB { c => (read andThen f)(c).read(c) }
}
object DB {
def pure[CTag, R](value : => R): DB[CTag, R] = DB { _ => value }
implicit def funcToDB[CTag, R](f : CTag #> R): DB[CTag, R] = DB(f)
}
trait Users
def getById(id : String): Users #> Option[DBObject] =
_.findOne(MongoDBObject(ID -> id))
def updateById(id : String, update : DBObject): Users #> Unit =
_.update(MongoDBObject(ID -> id), update)
def updateScore(id : String, newScore : Double): User #> Option[DBObject] =
...
def updateScore(id : String, newScore : Double): User #> Option[DBObject] =
for {
dboOpt <- getById(id)
update = for {
dbo <- dboOpt
games <- dbo.getAs[Long]("games")
score <- dbo.getAs[Double]("score")
gamesU = games + 1
scoreU = score + newScore
} yield $set("games" -> gamesU, "score" -> scoreU)
_ <- updateById(id, update)
updated <- getById(id)
} yield updated
Looks better but have a compile time error.
Type mismatch - updateById(id, update)
found: Option[DBObject]
required: DBObject
Special types that allow us to roll two containers DB & Option into a single one that shares the behaviour of both.
case class DBTOpt[CTag, R](run : DB[CTag, Option[R]]) {
def map[B](f : R => B): DBTOpt[CTag, B] = DBTOpt { DB { run(_) map f } }
def flatMap[B](f : R => DBTOpt[CTag, B]): DBTOpt[CTag, B] =
DBTOpt { DB { c => run(c) map f match {
case Some(r) => r.run(c)
case None => None
}}}
}
object DBTOpt {
def pure[CTag, R](value : => Option[R]): DBTOpt[CTag, R] =
DBTOpt { DB { _ => value } }
implicit def toDBT[CTag, R](db : DB[CTag, Option[R]]): DBTOpt[CTag, R] =
DBTOpt { db }
implicit def fromDBT[CTag, R](dbto : DBTOpt[CTag, R]): DB[CTag, Option[R]] =
dbto.run
}
def updateScore(id : String, newScore : Double): DB[Users, Option[DBObject]] =
for {
dbo <- DBTOpt { getById(id) }
games <- DBTOpt.pure { dbo.getAs[Long]("games") }
score <- DBTOpt.pure { dbo.getAs[Double]("score") }
gamesU = games + 1
scoreU = score + newScore
update = $set("games" -> gamesU, "score" -> scoreU)
_ <- updateById(id, update)
updated <- getById(id)
} yield updated
object resolve {
def apply[T, R](f : T => R)(implicit a : T) = f(a)
}
trait CollectionProvider[CTag] {
def apply[R](db : DB[CTag, R]): R
}
object CollectionProvider {
def apply[CTag](host : String, db : String, collection : String) =
new CollectionProvider[CTag] {
val coll = withTag[CTag](MongoClient(host)(db)(collection))
def apply[R](db : DB[CTag, R]): R = db(coll)
}
}
def parseDouble(s : String): Option[Double] = ...
def program(id : String): CollectionProvider[Users] => Unit =
ctx => {
println(s"Enter game result for '$id'")
parseDouble(readLine) map {
score => ctx(updateScore(id, score))
} map {
dbo => println(s"Updated: $dbo")
}
}
object inProduction {
implicit lazy val users =
CollectionProvider[Users]("localhost", "chess-online", "users")
implicit lazy val games =
CollectionProvider[Games]("localhost", "chess-online", "games")
}
import inProduction._
resolve(program("a@epam.com"))
The Reader monad (also called the Environment monad). Represents a computation, which can read values from a shared environment, pass values from function to function, and execute sub-computations in a modified environment.
Computations which read values from a shared environment.
Maintaining variable bindings, or other shared environment.
type Reader[E, A] = ReaderT[Id, E, A]
Monad transformers: special types that allow us to roll two monads into a single one that shares the behaviour of both. We will begin with an example to illustrate why transformers are useful and show a simple example of how they work.
type ReaderT[F[_], E, A] = Kleisli[F, E, A]
import scalaz._, Scalaz._
//usage already defined in scalaz: type =?>[E, A] = ReaderT[Option, E, A]
object RTOpt extends KleisliFunctions with KleisliInstances {
def apply[A, B](f : A => Option[B]): A =?> B = kleisli(f)
def pure[A, B](r : => Option[B]): A =?> B = kleisli(_ => r)
}
type MC[Tag] = MongoCollection @@ Tag
implicit def toR[Tag, R](f : MC[Tag] => R) = Reader(f)
implicit def toRTOpt[Tag, R](f : Reader[MC[Tag], Option[R]]) = RTOpt(f)
def updateScore(id : String, newScore : Double): Users #> Option[DBObject] =
for {
dbo <- RTOpt { getById(id) }
games <- RTOpt.pure { dbo.getAs[Long]("games") }
score <- RTOpt.pure { dbo.getAs[Double]("score") }
gamesU = games + 1
scoreU = score + newScore
update = $set("games" -> gamesU, "score" -> scoreU)
_ <- updateById(id, update)
updated <- getById(id)
} yield updated