-
Notifications
You must be signed in to change notification settings - Fork 16
Text
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
Appendwhere other APIs useWrite - Uses
AppendFormatwhere other APIs use overloads TextWriter- Not-so-helpfully returns
voidfromWritecalls - Just-different-enough write signatures from
StringBuilderto be annoying Stream- Direct API is sparse - requires
StreamWriter(with the same drawbacks asTextWriter) - 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.
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();
}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.