Say you want to extend a particular class (type) with some new functionality but you can't access the source code.. or perhaps you can but you don't want to use inheritance and subtype polimorphism..
Let's suppose you have the following type that represents an uploaded picture
case class Picture(name: String, uri: Uri)
and you want to return the json representation of it to your clients.
You can do something like this
trait Jsonable{
def toJson: JValue
}
final case class Picture(name: String, uri: Uri) extends Jsonable{
def toJson(): JValue = {
//implementation omitted
}
}
Then you realize that you need another type, Attachment.
No problem, let's add another case class to the mix
final case class Attachment(name: String, uri: Uri, visibile: Boolean) extends Jsonable{
def toJson(): JValue = {
//implementation omitted
}
}
Now you can serialize to json all of your case classes that extends the Jsonable trait
val picture: Jsonable = Picture("scalaitaly.png", new Uri("aws.s3.scalaitaly.png"))
val attachment: Jsonable = Attachment("speakers.pdf", new Uri("aws.s3.speakers.pdf"), true)
println(picture.toJson)
println(attachment.toJson)
This all works, but what if you don't have access to the source code of Picture
or Attachment
?
Is there another way? Maybe more Scala-ish?
Type Classes to the rescue
First and foremost. A Type Class has nothing to do with the concept of class in a typical Object Oriented programming language.
It's a concept from the Haskell world and it's used to achieve ad-hoc polymorphism
Type Classes in practice
The first thing to do is define the behavior that you want your classes to have, in this case we want our case classes to be serializable in the json format.
That's the Type Class.
trait JsonWriter[T]{
def toJson(in: T): JValue
}
then we need the Type Class instances for each concrete class we have
implicit val pictureJsonWriter = new JsonWriter[Picture]{
override def toJson(in: Picture): JValue = ???
}
implicit val attachmentJsonWriter = new JsonWriter[Attachment]{
override def toJson(in: Attachment): JValue = ???
}
we mark them implicit
because we want the compiler to be able to inject the correct implementation into the interface the we are defining next
def jsonOf[T](in: T)(implicit jsonWriter: JsonWriter[T]): String =
jsonWriter.toJson(in)
You can play with the code below
Wouldn't it better if the method toJson
would be a part of the classes in our domain model but without touching them?
Adding the functionality magically from the outside.
I would like to write
println(Picture("mypic", "mypicurl").toJson)
println(Attachment("myattachment", "myurl", false).toJson)
As if toJson
were a built-in method of the class.
Yes, I know that, for example in Ruby, Javascript you can do monkey patching, but we are talking about compile time transformation, not runtime.
In Scala you can achieve that using Implicit Classes
You just need to write
implicit class JsonSerializer[T](data: T){
def toJson(implicit writer: JsonWriter[T]): String =
writer.toJson(data)
}
This way, when you try to do something like Picture("mypic", "mypicurl").toJson
, the compiler, not finding the toJson
method in the Picture
class, tries to convert Picture
in a JsonSerializer
that has the method you want.
Then it passes as implicit writer: JsonWriter[T]
your Type Class instance that you defined for Picture
before.
You can play with the final code below
Conclusions
With this powerful design pattern we have achieved an impressive result.
We can now add new data or new methods to existing types without changing any existing code.
It's certainly a design pattern that is useful to have in your toolbelt.
It's so important that many famous libraries such as Shapeless, Cats, Scalaz and so on, couldn't even exists without this.
If you want to learn more (yes you want) I suggest you Advanced Scala by the amazing guys of Underscore.io.
Moreover, they recently open sourced all their ebooks, you can download all of them for free here, what are you waiting for?
Keep Learning
Until next time