Monday, October 19, 2015

Harness Scala Type Classes and Implicits

In my previous blog, I presented an Expression ADT. In this article I will extend its functionality and serialize it using other ADT while maintaining decoupling using Scala's magic a.k.a implicits and type classes . Full code is in this repository.
Let's start by building our JSON serializer ADT
sealed trait JSON
case class JSeq(elms:List[JSON]) extends JSON
case class JObj(bindings:Map[String,JSON]) extends JSON
case class JNum(num:Double) extends JSON
case class JStr(str:String) extends JSON
case class JBool(b:Boolean) extends JSON
case object JNull extends JSON
and now we can create our JSONWriter to convert JSON objects to nice JSON String
object JSONWriter{
  def write(j:JSON):String = {
    j match {
      case JSeq(e) => e.map(write).mkString("[",",","]")
      case JObj(obj) => 
           obj.map(o=> "\""+o._1+"\":"+write(o._2)).mkString("{",",","}")
      case JNum(n) => n.toString
      case JStr(s) => "\""+s+"\""
      case JBool(b) => b.toString
      case JNull => "null"

    }
  }
In order to make this JSONwriter work we need to add functionality to our Expression ADT that will handle the conversion to JSON object and then send it to the JSONWriter.write. Something like this
sealed trait AST{
def asJSON:JSON
}
and then implement asJSON in every subclass e.g
sealed trait BooleanExpression extends AST
case class BooleanOperation(op: String, lhs: BooleanExpression, rhs: BooleanExpression) extends BooleanExpression{

override def asJSON:JSON =  JObj(
        Map(
        "Operation"-> JStr(op),
        "lhs" -> toJSON(lhs),
        "rhs" -> toJSON(rhs)
        ))
}

// now we can call JSONWriter.write(BooleanOperation(..,..,..).asJSON)
That will do it right ? Wrong ! we want to maintain decoupling as much as possible. Our AST shouldn't care about the JSONWriter. we do not want it polluting the whole namespace.

Let's Harness Type Classes.

Type class defines the behavior in form of operation that must be supported by type T and allows ad-hoc polymorphism.
trait JSONSerializer[T] {
  def toJSON(value:T):JSON
}
And now we can add some functionality to use this trait to our JSONWriter object
...
 def write[A](value:A, j:JSONSerializer[A]):String = write(j.toJSON(value))
and use it like this
val astExpressionToJSON = new JSONSerializer[AST] {

      override def toJSON(value: AST):JSON = value match{

        case BooleanOperation(op, lhs, rhs) => JObj(
        Map(
        "Operation"-> JStr(op),
        "lhs" -> toJSON(lhs),
        "rhs" -> toJSON(rhs)
        ))
        case LRComparison ....

JSONWriter.write(BooleanOperation(..,..,..),astExpressionToJSON)
Well... that will do it, but you might ask yourself so where is the Scala magic you talked about ? We want to maintain our code as clean and in style as much we can. Implicits can be very helpful in this case. We just need to change the write method in the JSONWriter object

Implicits - the hidden gem

Just as a quick brush up, and I'm stepping aside her we can define an implicit parameter and tell the compiler to search for it somewhere else in scope for example.
implicit val i = 1 
//somewhere else in the code we can define a method that will use this Int 

def increment(x:Int)(implicit y:Int) = x+y
As long that the implicit definition is in the scope we can use it without the second parameter
scala> implicit val i = 1
i: Int = 1

scala> def increment(x:Int)(implicit y:Int) = x + y
increment: (x: Int)(implicit y: Int)Int

scala> increment(9)
res0: Int = 10
Warning! Do not abuse Implicits . Implicits, if not used wisely can seriously damage your code readability. Sometimes, you can simply add a default value.
It always depends how and what do you want to express .
Saying that, let's get back on track.
...
def write[A](value:A)(implicit j:JSONSerializer[A]):String = write(j.toJSON(value))
...
and that's it (well almost, more cool stuff ahead) . with this defined we need to create our implicit definition
   implicit val astExpressionToJSON = new JSONSerializer[AST] {

   override def toJSON(value: AST):JSON = value match{
   case BooleanOperation(op, lhs, rhs) => JObj(
       Map(
       "Operation"-> JStr(op),
       "lhs" -> toJSON(lhs),
       "rhs" -> toJSON(rhs)
       ))
   case  LRComparison (lhs: Variable, op, rhs: Constant) =>JObj( ....
and we are good to go, nice and clean.
val parser = new ConditionParser {}
val p:AST = parser.parse("foo = '2015-03-25' || bar = 8").get //I know that the "get" will succeed here
val s = JSONWriter.write(p)
println(s)
//will print {"Operation":"||","lhs":{"Operation":"=","lhs":"foo","rhs":"1422136980000"},"rhs":{"Operation":"=","lhs":"bar","rhs":8.0}}

Context Bound

Another way to achieve the same with a stylish way to use implicitly pulls the implicit from the context is using context bound by stating that A is a member of the type class hence it must have the functionality that we want.
def toJSONString[A:JSONSerializer](value:A):String = {
    val j = implicitly[JSONSerializer[A]]//pull the implicit def of JSONSerializer
    write(j.toJSON(value))
    //we can replace the upper two lines with:  write(implicitly[JSONSerializer[A]].toJSON(value))
  }
and use it like this
val parser = new ConditionParser {}
val p:AST = parser.parse("foo = '2015-03-25' || bar = 8").get //I know that the get will succeed here
val s1 = JSONWriter.toJSONString(p)
println(s1)
//will print {"Operation":"||","lhs":{"Operation":"=","lhs":"foo","rhs":"1422136980000"},"rhs":{"Operation":"=","lhs":"bar","rhs":8.0}}
we can use it everywhere (YEY!!!) lets say that we have a 3rd party class e.g Person class we can add this functionality without changing the 3rd party class.
case class Person (name:String,age:Int)

object PersonImplicits{
  implicit val personToJSON = {
    new JSONSerializer[Person] {
       override def toJSON(value: Person): JSON =JObj( Map("name" -> JStr(value.name),
       "age" -> JNum(value.age)))
    }
  }
}

// and just bring that into scope 

    import PersonImplicits._
    val person = Person("Drakula",578)
    println(JSONWriter.write(person))
//will print {"name":"Drakula","age":578.0}

Implicit classes

another cool use of implicits is to add functionality to external class using implicit class
object JSONConverterImplicits {
  implicit class ASTConverter(ast:AST){
    def asJSON: JSON = ast match{

      case BooleanOperation(op, lhs, rhs) => JObj(
        Map(
          "Operation"-> JStr(op),
          "lhs" -> lhs.asJSON,
          "rhs" -> rhs.asJSON
        ))
      case  LRComparison (lhs: Variable, op, rhs: Constant) =>JObj( ...
and now we can use it as is "asJSON" is part of the AST
    import JSONConverterImplicits._
    val p2 = parser.parse("foo = '2015-03-25' || bar = 8 ").get
    val s2 = JSONWriter.write(p2.asJSON)
how cool is that ?!

Summary 

Type classes and implicits are very powerful tools, especially when writing libraries that needs to be extended, and maintaining decoupled. We can add default implementations in a nice and natural way for the developers that uses our library. Those implementations can easily overwrite and extended.

hope you enjoyed it.
  • Comments and feedbacks are always welcome

2 comments:

  1. Extending 3rd party libraries is where implicit really shine.
    Good article.

    ReplyDelete