toString considered harmful, part 3
This is part 3 of a series of articles, “
toString considered harmful”.
Languages without the
There actually are quite a few languages that don’t have the
toString problem, or at least have it to a lesser degree.
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++ 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
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 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 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)
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, 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!
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.