Guest tip from Webfreak about toString

Posted 2023-02-13

Tip of the week about how to define toString and some preview about new dub features.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

Tip of the Week

Today's tip is a contribution from WebFreak. Thanks to them for writing this! (I did some light editing.)

There are a bunch of ways how to define custom toString methods. The methods are not exactly part of D, but rather what is the convention in Phobos:

when using .to!string, it does the following:

  • Object to string conversion calls toString against the object or returns "null" if the object is null.
  • Struct to string conversion calls the struct's toString method if it is defined according to std.format.write's guidelines.

From the std.format.write documentation:

Aggregate types can define various toString functions. If this function takes a FormatSpec or a format string as argument, the function decides which format characters are accepted ...

toString should have one of the following signatures:

void toString(Writer, Char)(ref Writer w, const ref FormatSpec!Char fmt)
void toString(Writer)(ref Writer w)
string toString();

Where Writer is an output range which accepts characters (of type Char in the first version). The template type does not have to be called Writer.

Sometimes it's not possible to use a template, for example when toString overrides Object.toString. In this case, the following (slower and less flexible) functions can be used:

void toString(void delegate(const(char)[]) sink, const ref FormatSpec!char fmt);
void toString(void delegate(const(char)[]) sink, string fmt);
void toString(void delegate(const(char)[]) sink);

When several of the above toString versions are available, the versions with Writer take precedence over the versions with a sink. string toString() has the lowest priority.

Now I think the easiest way to define a toString method in structs is simply like this:

string toString() const @safe {
	import std.conv : text;

	// for example use text, format!"", or just plain old concatenation
	return text(fieldA, ", ", fieldB, ", ", etc);

You can use the toString(Writer)(ref Writer w) interface as well, however I would recommend against this, as any errors you make in there (e.g. typoing a function or doing anything that would cause a compilation error) makes .to!string and format no longer use that function, effectively making it worthless.

One use-case I do like here is combining the two however:

string toString() const @safe {
	import std.array : appender;

	auto ret = appender!string;
	this.toString(ret);
	return ret.data;
}

void toString(W)(ref W writer) const @safe {
	writer.put(fieldA);
	writer.put(", ");
	writer.put(fieldB);
	writer.put(", ");
	writer.put(etc);
}

one interesting other possible overload to override strings is using the callback/sink based approach:

void toString(void delegate(const(char)[]) sink)
{
	sink(fieldA);
	sink(", ");
	sink(fieldB);
	sink(", ");
	sink(etc);
}

A disadvantage with this approach is that you can't mark your function @safe, nothrow, pure, etc. without also restricting the sink to mirror your attributes. However compiler errors do get caught here, so it at least has some merit over solely using the Writer based approach.

I think defining a Writer toString method with a second toString calling the writer with an appender!string is a solid middle-ground that should catch most bugs and also fall back to using an appender if it does fail to compile with some exotic writer type.

I think the writer overload calling could be improved in the future as well. It should not check for __traits(compiles, ...) or is(typeof(...)), but rather check if a method with the given signature exists, and then just try to call it. (maybe we need some trait to see if a potential matching function overload exists, without trying to compile it?)

Now there is one more thing we can do with the sink and writer based approach: custom Format Specifiers. To learn more about these, see this page on the wiki about defining custom print format specifiers.

Dub improvements

WebFreak also gave me a preview of some upcoming changes to dub. One is a new feature called dub select which lets you easily specify a particular version of a package, and the other is is dub upgrade -l which will show you the old and new proposed versions before it actually makes the change. These are not yet merged so the exact details are subject to change before it is released.