toString considered harmful, part 2
Dec 26, 2013 · 4 minute read · CommentsScalaJavaHaskellStandard MLOCamlCC++CsharpRubyPythonLispSchemeGoobject-orientedstring interpolationimplicitstype classes
This is part 2 of a series of articles, “toString
considered harmful”. [Part 1] introduced the problem in the context of a common design flaw present in object-oriented languages, and proposed a simple workaround.
In part 2, we look at advanced ways to organize “stringable” data, using either an object-oriented or functional style. Examples are in Scala because it equally supports either style.
Object-oriented vs. functional
The fix presented was in object-oriented style, adding a method toUrlString
to a class. The other solution is the functional style, leaving the Id
class alone, and writing an external function instead:
case class Id(id: Int)
def toUrlString(id: Id) = id.toString
with
id match {
case None => println("No id found!")
case Some(n) => getUrl(makeUrl(toUrlString(n)))
}
There are advantages and disadvantages to either solution.
More advanced OO
It would be very natural, given a whole set of domain classes in addition to Id
, to want all of them to have a toUrlString
. Then the natural thing to do is to create a mini-universe (parallel to the toString
universe) by creating a hierarchy:
trait UrlString {
def toUrlString: String
}
case class Id(id: Int) extends UrlString {
override def toUrlString = id.toString
}
case class ...(...) extends UrlString {
override def toUrlString = ...
}
Advanced string interpolation with OO
In fact, Scala and other languages with advanced string interpolation facilities allow yet another refactoring by making sure that what gets into a URL isn’t just an arbitrary string in the first place!
Below we define a string interpolator that only operates on objects of classes that implement the trait UrlString
, and therefore does away with an explicit call to toUrlString
:
implicit class UrlHelper(val sc: StringContext) extends AnyVal {
def url(args: UrlString*): String = {
val strings = sc.parts.iterator
val expressions = args.iterator
val buf = new StringBuffer(strings.next)
while (strings.hasNext) {
buf append expressions.next.toUrlString
buf append strings.next
}
buf.toString
}
}
/** Only ever use UrlString to create a URL. */
def makeUrl(id: UrlString): String = url"http://service.com?id=$id"
with
id match {
case None => println("No id found!")
case Some(n) => getUrl(makeUrl(n))
}
This may or may not be overengineering.
Advanced string interpolation with FP
The functional approach doesn’t like inheritance in the domain classes. We can implement it with type classes (a concept first pioneered in Haskell in the late 1980s) by means of implicits in Scala, in order to implement toUrlString
outside of a class hierarchy but also allow it to be used in a constrained generic way. A full discussion of this is outside the scope of this post, but the basic point is that with type classes, one can write code that does not depend on an inheritance hierarchy. If you’re used to monkey-patching in dynamic languages, think of it as compile-time monkey-patching.
// A type class
trait UrlString[A] {
def toUrlString(a: A): String
}
// Wrapper class
case class Id(id: Int)
// Implement the type class UrlString for Id
implicit object IdToUrlString extends UrlString[Id] {
override def toUrlString(a: Id) = a.id.toString
}
implicit class UrlHelper(val sc: StringContext) extends AnyVal {
def url[A: UrlString](args: A*): String = {
val strings = sc.parts.iterator
val expressions = args.iterator
val buf = new StringBuffer(strings.next)
while (strings.hasNext) {
buf append implicitly[UrlString[A]].toUrlString(expressions.next)
buf append strings.next
}
buf.toString
}
}
/** Anything "viewable" as UrlString can be used to create a URL. */
def makeUrl[A: UrlString](id: A): String = url"http://service.com?id=$id"
The final string gotcha
You may have noticed that there is still primitive obsession in this sample code: URLs are presented as String
for simplicity. In real life, I use builders such as URIBuilder
and HttpGet
(Java Apache HttpComponents) or more sophisticated Scala-specific libraries such as Spray.
Conclusion
I thought it would useful to compare an object-oriented and a functional approach to unifying data that share a domain-consistent notion of conversion to a string. Scala is a language that allows easy expression of both.
In part 3, we will look at languages that just don’t have the toString
problem at all.