Thursday, March 16, 2017

Render unto Caesar...

There's a minor issue with both Java and Scala, at least Scala out-of-the-box. That is, while there is a way to turn any object into a String for debugging purposes (it's the toString method available in both Java and Scala), there is no easy, consistent way to turn an object into a String that can be used for presentation.

In a former life, I have created methods to do this for Java. But it's frustrating at best. In Java, if you want a class to take on some particular behavior, you have to arrange for that class to extend an interface that defines the behavior. If that class, some sort of collection for instance, isn't under your control, you're out of luck.

Not so in Scala!  You can add behavior to a (someone else's) library class through the magic of implicits.

So, let's say we define the presentation version of toString in a trait called Renderable, with one method render.

trait Renderable {
  def render: String
}

Any class that you want to be able to prettify in output can extend Renderable. So far, so good. But suppose that you have a list of Renderable objects. It's not much use if you can make the elements pretty but not the whole list. You could, for example, define a RenderableSeq as follows:

case class RenderableSeq[A <: Renderable](as: Seq[A]) extends Renderable {
  def render: String = as map (_ render) mkString ","
}

But, apart from the fact that this is rather ugly and involves creating a set of special containers with which to wrap your sequences, it's simply impractical. Much of the time, you will be dealing with containers that are elements of other classes and you won't be able to inject your desired behavior.

This is where implicits come to the rescue.

import scala.util._

trait Renderable {
  def render: String
}

case class RenderableTraversable(xs: Traversable[_]) extends Renderable {
  def render: String = {
    def addString(b: StringBuilder, start: String, sep: String, end: String): StringBuilder = {
      var first = true
      b append start
      for (x <- xs) {
        if (first) {
          b append Renderable.renderElem(x)
          first = false
        }
        else {
          b append sep
          b append Renderable.renderElem(x)
        }
      }
      b append end
      b
    }
    addString(new StringBuilder, "(\n", ",\n", "\n" + ")").toString()
  }
}

case class RenderableOption(xo: Option[_]) extends Renderable {
  def render: String = xo match {
    case Some(x) => s"Some(" + Renderable.renderElem(x) + ")"
    case _ => "None"
  }
}

case class RenderableTry(xy: Try[_]) extends Renderable {
  def render: String = xy match {
    case Success(x) => s"Success(" + Renderable.renderElem(x) + ")"
    case _ => "Failure"
  }
}

case class RenderableEither(e: Either[_, _]) extends Renderable {
  def render: String = e match {
    case Left(x) => s"Left(" + Renderable.renderElem(x) + ")"
    case Right(x) => s"Right(" + Renderable.renderElem(x) + ")"
  }
}

object Renderable {
  implicit def renderableTraversable(xs: Traversable[_]): Renderable = RenderableTraversable(xs)

  implicit def renderableOption(xo: Option[_]): Renderable = RenderableOption(xo)

  implicit def renderableTry(xy: Try[_]): Renderable = RenderableTry(xy)

  implicit def renderableEither(e: Either[_, _]): Renderable = RenderableEither(e)

  def renderElem(elem: Any): String = elem match {
    case xo: Option[_] => renderableOption(xo).render
    case xs: Traversable[_] => renderableTraversable(xs).render
    case xy: Try[_] => renderableTry(xy).render
    case e: Either[_, _] => renderableEither(e).render
    case r: Renderable => r.render
    case x => x.toString
  }
}

With this design, you can simply invoke render on any object that is either Renderable itself or one of the four implicit Renderable containers defined above. A complex class with embedded options, sequences, whatever simply has to extend Renderable and all elements that can be rendered thus will be (other classes will simply be converted to String via toString). Your application code hardly needs to be aware of the Renderable trait because the implicit converters are defined, implicitly, in the Renderable companion object.

In practice, this particular render method isn't all that useful. I defined it thus to simplify the logic of the implicit definitions. In my LaScala repository on Github [currently only defined in branch V_1_0_1 but soon to be merged with the master branch], I have defined a rather more sophisticated version as follows:

trait Renderable {

  /**
    * Method to render this object in a human-legible manner.
    *
    * @param indent the number of "tabs" before output should start (when writing on a new line).
    * @param tab    an implicit function to translate the tab number (i.e. indent) to a String of white space.
    *               Typically (and by default) this will be uniform. But you're free to set up a series of tabs
    *               like on an old typewriter where the spacing is non-uniform.
    * @return a String that, if it has embedded newlines, will follow each newline with (possibly empty) white space,
    *         which is then followed by some human-legible rendition of *this*.
    */
  def render(indent: Int = 0)(implicit tab: Int => Prefix): String
}
case class Prefix(s: String) {
  override def toString: String = s
}

object Prefix {

  /**
    * The default tab method which translates indent uniformly into that number of double-spaces.
    *
    * @param indent the number of tabs before output should start on a new line.
    * @return a String of white space.
    */
  implicit def tab(indent: Int): Prefix = Prefix(" " * indent * 2)
}

The other classes are more or less exactly as shown above, but with the more complex version of render supported. It would be possible to add further complexity, for example letting the render method choose whether to use newlines rather than commas--according to the space available. But so far, I haven't felt that the extra complexity was really justified.

No comments:

Post a Comment