toString considered harmful, part 3
Dec 27, 2013 · 4 minute read · CommentsScalaJavaHaskellStandard MLOCamlCC++CsharpRubyPythonLispSchemeGoobject-orientedstring interpolationtype classes
This is part 3 of a series of articles, “toString
considered harmful”.
Languages without the toString
problem
There actually are quite a few languages that don’t have the toString
problem, or at least have it to a lesser degree.
C
There is no friendly generic conversion to a string (uh, really just a pointer to a null-terminated chunk of char
) in C. The closest thing is using the type-unsafe printf
family of functions, but you have to do almost all the work if you have some complicated struct
and want to turn it into a C string.
C++
C++ introduced iostreams, where f you follow certain conventions and overload operator<<
for every domain class of interest, you can build up decent looking strings, without using inheritance.
Or you could do the object-oriented thing and set up a hierarchy with a ToString
abstract base class. But C++ does not come with everything already inheriting from an Object
.
Haskell
Haskell does not force a toString
on everything, but provides a Show
type class for convenience in the standard prelude. It is easy (and convenient for debugging) to just tack on deriving Show
and then call show
to convert stuff to a string. This means that one can get lazy and fall into the same design traps as mentioned in the very first code example above. Again, the solution is to refuse to abuse show
, and to use a different name instead for converting something to a string for a particular purpose.
Go
Go was invented at Google as a modernized C. It does not have classes, but does have dynamic interfaces. All that is required for a user-defined type to satisfy an interface is to implement the method String()
returning a string
. Basically, this makes the type implement the interface fmt.Stringer. Again, if you don’t implement String() string
for your type, then you will get a compile-time error when trying to treat it as a string.
Superficially, this sounds like Haskell type classes, but it’s much more limited, because Go does not have generics and Go is only single dispatch. Go’s interfaces really implement a kind of structural subtyping.
Standard ML
Standard ML does not have the toString
problem. It does, by convention, supply a toString
function in many modules in the Standard ML Basis Library, such as Int and Real and Bool, but these are just conventions and do not participate in any kind of unified conversion to string. If you want to convert anything besides a primitive type to a string, you have to write the conversion function yourself.
Furthermore, Standard ML, as a rather opinionated and “purist” language, designed specifically for static simplicity, semantic minimalism, and runtime efficiency, does not believe in type classes, so there is no way to write a function that at runtime is generic over what can be turned into a string.
The best you can do is write something that is functorized, but then you have to apply it in a statically known context:
signature TO_STRING =
sig
type t
val toString : t -> string
end
functor DoStuff(ToString : TO_STRING) =
struct
fun doubleString (stuff: ToString.t) =
let
val s = ToString.toString stuff
in
s ^ s
end
end
structure MyStuff : TO_STRING =
struct
type t = int * bool
fun toString (i, b) =
"(" ^ Int.toString i ^ ", " ^ Bool.toString b ^ ")"
end
structure DoMyStuff = DoStuff(MyStuff)
with
DoMyStuff.doubleString (42, true)
(* result is the string "(42, true)(42, true)" *)
Since the Standard ML ecosystem is so minimalist, it’s hard to fall into the toString
trap, because you would have to set it all up yourself.
OCaml
OCaml, like Standard ML, does not provide a generic toString
out of the box, but the OCaml ecosystem is much more practically oriented.
There is a pre-processor for OCaml that can be used to generate convenient printers for types, deriving
. There is also an S-expression based generator, Sexplib. But these are mechanically generated, rather than part of something generic at runtime.
Of course, one could also use the object-oriented part of OCaml to make a generic “to string” hierarchy starting with a suitable interface:
class type convert_to_string =
object
method to_string : string
end
But I don’t actually know many people who use the object-oriented features of OCaml!
Conclusion
An annoying bug I temporarily created in my code led me to take stock of the state of toString
design choices in various programming languages, and also consider how we can better escape fragility in our code, independent of whatever language we are using.