Functional refactoring in Scala

by  Yuriy Polyulya  /  @polyulya  /  +blog

Some update method for:

Chess game player score update method. For player with: id, name, last name, games and score fields stored in MongoDB.


User class:


case class User(
  id        : String,
  name      : String,
  lastName  : String,
  games     : Long,
  score     : Double)
        

Database document:


{ "_id" : "yuriy_polyulya@epam.com",
  "name" : "Yuriy",
  "last-name" : "Polyulya",
  "games" : 15,
  "score" : 100.0 }
        

Can look like:


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
  }
}
        

Design Issues:


  • 1. Tight-coupling:

    
      val users = MongoClient("localhost")("chess")("users")
              

  • 2. Complexity:

    
      ... match {
            case Some(dbo)  =>
              ... match {
                  case (Some(g), Some(s)) =>
                    ...
                  case _ => None
              }
            case None => None
        }
              

  • 3. Direct fields using, more...

Tight-coupling fix:


  • 1. Service Locator (global collection factory/storage):

    
    object ChessMongoCollections {
      lazy val users = MongoClient("localhost")("chess")("users") // or def
    }
    
    ...
    
    val users = ChessMongoCollections.users
                

  • 2. Dependency Injection:

    
    def updateScore(
      id : String,
      gameScore : Double,
      users : MongoCollection): Option[DBObject] = {
    
    ...
    
    }
                

Dependency injection after curring:


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
    }
  }
          

Mongo-Collection Reader:

Reader - for computations which read values from a shared environment.



case class DB[R](read : MongoCollection => R) {
  def apply(c : MongoCollection): R = read(c)
}
          

  • Possible issue:

    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)
                  

Solution:

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)
}
          

Mongo-Collection Reader:

Extend DB Reader for: pass values from function to function, and execute sub-computations in a modified environment.


  • Lift exist function to Reader:

    
    case class DB[CTag, R](read : CTag #> R) {
      ...
    
      def map[B](f : R => B): DB[CTag, B] = DB { read andThen f }
    }
                

  • Combine two Readers:

    
    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) }
    }
                

Mongo-Collection Reader:


DB Reader class:


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) }
}
        

DB Reader object (for "pure" method):


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)
}
        

Update method with DB Reader:


Functionality decomposition:


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] =
  ...
          

Updated 'updateScore' method:


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

Mongo-Collection Reader Transformer:

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
    }}}
}
        

DB Transformer (for "pure" method and implicit conversion):


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
}
        

Update method with DB Reader & Transformer:



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
        

Dependency Resolver:


Resolver:


object resolve {
  def apply[T, R](f : T => R)(implicit a : T) = f(a)
}

          

DB interpreter:


trait CollectionProvider[CTag] {
  def apply[R](db : DB[CTag, R]): R
}
          

Companion with factory method:


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)
    }
}
          

Dependency injection:


Code:


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")
    }
  }

          

Concrete instances:


object inProduction {
  implicit lazy val users =
    CollectionProvider[Users]("localhost", "chess-online", "users")

  implicit lazy val games =
    CollectionProvider[Games]("localhost", "chess-online", "games")
}
          

"Injection":


import inProduction._

resolve(program("a@epam.com"))
          

Reader Monad:

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.


Computation type:

Computations which read values from a shared environment.


Useful for:

Maintaining variable bindings, or other shared environment.


Type:


type Reader[E, A] = ReaderT[Id, E, A]
        

Monad Transformer:

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:


type ReaderT[F[_], E, A] = Kleisli[F, E, A]
        

Reader Monad & Monad Transformer (Scalaz):



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)
}
        

And implicit conversions:


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)
        

Update method (with Scalaz):



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
        

  • Tight-coupling  - RESOLVED


  • Complexity      - RESOLVED

  • Questions?


  • Remarks?