Friday, June 18, 2010

Type Class pattern example

I was pleasantly suprised by the popularity of my last article, but some people expressed the wish to see a more practical example, outside Scala library. It is always difficult to use practical examples in blog articles, as they often require a lot of context to be properly understood and introduce a lot of unnecessary complexity.

So I'm going to compromise here. The example below discuss a common task in many systems, but which I'm going to present in a very simplistic manner. Specifically, how to format stuff for output.

There is much discussion about the proper place to handle output formatting. On one hand, for instance, we have the toString method available on every Java object. It is not uncommon to have serializer, toXML, or similar stuff either. On the other hand, such approach is not scalable and mixes business rules with presentation concerns, and architectures such as Model-View-Controller try to avoid it.

So let's try to solve this problem using the type class pattern. First, let's create or type class for a formatter which produces XML output (NodeSeq class), just as described in the last article. This time, there is one method we want this type class to have, though. Here it is:

import scala.xml.NodeSeq

abstract class FormatterXML[T] {
def format(input: T): NodeSeq
}

To test it, we'll create a very simple method that takes this type class and use it to print stuff to the console.

def output[T](what: T)(implicit formatter: FormatterXML[T]) = println(formatter.format(what))

And, to finish the initial example, we'll create a simple class, good old Person, to test it. We'll put the implicit type class object inside the companion object of Person, so that it can be found automatically. Already, the class Person has become free of concerns about formatting, though companion object might be too close for some. This, however, is not necessary, as we'll see later.

In the code below, as in other examples later on, I'm declaring the case class and the companion object in the same line, so that it can be easily pasted on REPL (which won't recognize the companion object as a companion object otherwise). I added a test line too at the end.

case class Person(name: String, age: Int); object Person {
implicit object Formatter extends FormatterXML[Person] {
def format(input: Person): NodeSeq = <Person><Name>{input.name}</Name><Age>{input.age}</Age></Person>
}
}

output(Person("John", 32))

So far, this can be trivially done in other ways, so we are gaining nothing. We'll gradually increase the complexity so that benefits of this approach start to show up. First, we'll create a Song class which has a Person member, so that we show off composition of the formatter.

case class Song(title: String, author: Person); object Song {
implicit object Formatter extends FormatterXML[Song] {
def format(input: Song): NodeSeq = <Song>
<Title>{input.title}</Title>
<Author>{implicitly[FormatterXML[Person]].format(input.author)}</Author>
</Song>
}
}

output(Song("War Pigs", Person("Ozzy Osbourne", 61)))

Now, I'm sure all of you must be up in arms right now, because, as everyone knows, Ozzy Osbourne wasn't the sole author of War Pigs. We need to change the Song class to accomodate more authors, but that will make the formatter more complex. To help us here, let's create a formatter for generic lists, that we can use to simplify the Song formatter:

def ListFormatterXML[T](implicit formatter: FormatterXML[T]) = new FormatterXML[Traversable[T]] {
def format(input: Traversable[T]): NodeSeq = <List>{input map formatter.format}</List>
}

Here we start to see an advantage to this approach. This method will work for any list (any traversable, in fact!), as long as there is a formatter for its members. Anyway, let's see the Song again:

case class Song(title: String, authors: Person*); object Song {
implicit object Formatter extends FormatterXML[Song] {
def format(input: Song): NodeSeq = <Song>
<Title>{input.title}</Title>
<Authors>{ListFormatterXML[Person].format(input.authors)}</Authors>
</Song>
}
}

output(Song("War Pigs",
Person("Tony Iommi", 62),
Person("Ozzy Osbourne", 61),
Person("Geezer Butler", 60),
Person("Bill Ward", 62)))

This works, but having as the sole child of looks a bit weird. Let's do away with this:

def ListFormatterXML[T](kind: String)(implicit formatter: FormatterXML[T]) = new FormatterXML[Traversable[T]] {
def format(input: Traversable[T]): NodeSeq = <List>{input map formatter.format}</List>.copy(label = kind)
}

case class Song(title: String, authors: Person*); object Song {
implicit object Formatter extends FormatterXML[Song] {
def format(input: Song): NodeSeq = <Song>
<Title>{input.title}</Title>
{ListFormatterXML[Person]("Authors").format(input.authors)}
</Song>
}
}

output(Song("War Pigs",
Person("Tony Iommi", 62),
Person("Ozzy Osbourne", 61),
Person("Geezer Butler", 60),
Person("Bill Ward", 62)))

Personally, I like this better, but your milleage may vary. Anyway, let's increase complexity a bit. If we want scalability, we'd better make the formatter more flexible, able to handle many different formats. Let's redefine our type class, in a way that avoids changes to the code we have written so far:

abstract class Formatter[T, Format] {
def format(input: T): Format
}

abstract class FormatterXML[T] extends Formatter[T, NodeSeq] {
def format(input: T): NodeSeq
}

We also need a new output, but here we have a problem. How do we specify the format type explicitly while, at the same time, inferring the input type? I'll confess not having given much thought to this problem, but the following code will work:

def as[F] = new {
def output[T](what: T)(implicit formatter: Formatter[T, F]) = println(formatter.format(what))
}

With this new definition in place, we can recompile Person, Song and ListFormatterXML above, and try it out like this:

as[NodeSeq].output(Song("War Pigs", 
Person("Tony Iommi", 62),
Person("Ozzy Osbourne", 61),
Person("Geezer Butler", 60),
Person("Bill Ward", 62)))

And, without changing any of that code, let's add a new output format to conclude this article:

implicit object PersonFormatterString extends Formatter[Person, String] {
def format(input: Person): String = "%s, age %d" format (input.name, input.age)
}

def ListFormatterString[T](kind: String)(implicit formatter: Formatter[T, String]) = new Formatter[Traversable[T], String] {
def format(input: Traversable[T]): String = input map formatter.format mkString (kind+" ", ", ", "; ")
}

implicit object SongFormatterString extends Formatter[Song, String] {
def format(input: Song): String = input.title+" "+ListFormatterString[Person]("by").format(input.authors)
}

as[String].output(Song("War Pigs",
Person("Tony Iommi", 62),
Person("Ozzy Osbourne", 61),
Person("Geezer Butler", 60),
Person("Bill Ward", 62)))

8 comments:

  1. One non-standard-library place where this pattern is within Lift, in the Binding framework. The source code of the interesting bits is here. Note that here, the "implicit typeclass" is simply a type alias for a curried function with the signature T => NodeSeq => NodeSeq.

    ReplyDelete
  2. Nice post (the previous one also). Thanks for sharing.

    ReplyDelete
  3. Thanks for the nice posts on the topic!

    I think for the case where two type parameters are needed the following will also work:

    def output[T, F](what: T)(implicit formatter: Formatter[T, F]) = ..

    The compiler will successfully infer both types.

    ReplyDelete
  4. There's no hint to let the compiler infer F. It will only do so if there's a single implicit available, otherwise I expect it to return a message about ambiguity.

    ReplyDelete
  5. Yeah right. I missed that ;-)

    ReplyDelete
  6. Very nice post Daniel! Thank you.

    I'm looking for a way to deserialize an XML file representing a cyclic class/object graph. Almost every class/object contains a id field which is used to represent (mutual) references when serializing to the XML file.
    Moreover, I would like to keep all my object/classes immutables!

    Do you have any hint to do the work properly ?

    Thanks by advance

    --
    Julien

    ReplyDelete
  7. I don't have any particular recommendation regarding immutable graphs, but you might want to look at the Graph project on Scala Incubator (https://scala-incubator.assembla.com/code/scala-graph/subversion/nodes).

    ReplyDelete
  8. Thanks! I'll take a look to it.

    I'm pretty in a hurry so I did not state my problem clearly, I'm sorry for that. I'll try to post it later with a first solution if you will.

    Nevertheless, thanks for answering to me here and to others on stackoverflow. I started learning Scala 2 weeks ago and I must say your answers are often helpful ;)

    --
    Julien

    ReplyDelete