Skip to content
Bryan Watts edited this page Feb 1, 2015 · 1 revision

Totem features the Text type, a unified medium for working with strings.

.NET has several options for building text, each with its own quirks:

  • StringBuilder
  • Helpfully returns itself from Append* calls
  • Uses Append where other APIs use Write
  • Uses AppendFormat where other APIs use overloads
  • TextWriter
  • Not-so-helpfully returns void from Write calls
  • Just-different-enough write signatures from StringBuilder to be annoying
  • Stream
  • Direct API is sparse - requires StreamWriter (with the same drawbacks as TextWriter)
  • Requires thinking about seek positioning

This is a decent amount of friction to navigate each time we want to build some text. We often fall back to procedural code, full of concatenation and String.Format, to avoid the overhead (and the benefits) of the tools at our disposal.

Digging further

Beneath this, though, lies a more subtle and nefarious truth: all of these techniques are eager. They represent the result of building text; the work happens immediately when the call is made.

The alternative is a deferred approach, one that represents the ability to build text only when required. This is the purpose of the Text type - represent an algorithm for producing text.

  • We can see the same approach to deferred execution throughout LINQ

The classic example scenario benefiting from this approach is writing to a log:

Log.Info("Something happened: " + expensiveObject.ToString());

If the log level is too low, we calculate that string just to ignore it. A common workaround is to check the level first:

if(Log.CanWriteInfo)
{
    Log.Info("Something happened: " + expensiveObject.ToString());
}

We avoid the expensive string, but at the cost of readability; who wants to see that block everywhere?

Next, we may try to defer evaluation by using a function:

Log.Info(() => "Something happened: " + expensiveObject.ToString());

This works and is reasonably readable; it does not, however, alleviate the friction of the other techniques:

Log.Info(() =>
{
    var builder = new StringBuilder();

    builder.Append("Something happened: ").AppendLine(expensiveObject);

    if(foo != null)
    {
        builder.AppendFormat(" [foo: {0}]", foo);
    }

    builder.AppendLine().AppendLine().AppendFormat(" [bar: {0}]", bar).AppendLine();

    return builder.ToString();
}

Using Text

The log write looks much nicer:

Log.Info(Text
    .Of("Something happened: ")
    .WriteLine(expensiveObject)
    .WriteIf(foo != null, Text.Of(" [foo: {0}]", foo))
    .WriteTwoLines()
    .WriteLine(" [bar: {0}]", bar));

If Log.Info never calls .ToString() on the Text instance it receives, the writes never happen, and we avoid the overhead of the expensive string and multiple format calls. We also get a much nicer API based on extension methods!

See the test scenarios for a comprehensive look into the capabilities of the Text type.

Clone this wiki locally