Monadic templating in C# - Part 2

11/10/2015 By Laszlo 0 comments

In the previous article (Part 1), I had introduced a prototype of a monadic framework for generating formatted output (e.g. source code) from some complex, hierarchical set of data (e.g. Abstract Syntax Tree of some input language). This second part picks up where the first part left, the explanation of the actual templating by utilizing the string interpolation feature of C# 6.0.

Introduction

Generating formatted output with lightweight templates has never been easier than with the string interpolation feature of C# 6.0. This feature renders some (used to be very useful) templating libraries, e.g. SmartFormat obsolete. String interpolation basically means that one can insert arbitrary expressions into a string by decorating it with curly brackets. We can finally forget String.Format and the numbered placeholders...

In this article, first, string interpolation is introduced briefly by contrasting it with some earlier approaches. Following that I'll show how to marry string interpolation with the previously introduced monadic generators.

Lightweight string templating

In the past we had two options for lightweight templates. The most obvious choice was the ubiquitous String.Format. It gets a template in its first argument, and uses varargs for passing the objects referenced by the template (the exact same idea used in C since the seventies). The template language is simplistic, references are indexes of the arguments wrapped into curly brackets (if one wants to output curly bracket characters, those must be duplicated for escaping):

1 String.Format("function {0}({1}){{{2}}}", name, arguments, body)

It is a very unpractical approach as it is very hard to read. It is mainly because there of the indirection in the template and that the indirection is achieved by numbered indexes. Numbered indexes are great for computers, but highly demotivating for humans.

A better approach is what used by e.g. SmartFormat, named placeholders:

1 Smart.Format("function {name}({arguments}){{{body}}}", function)

Unfortunately, this feature is limited. It works only if the template has exactly one object argument (the names in the template correspond to the members of the object then). It helps a bit that anonymous types can be involved:

1 Smart.Format("function {name}({arguments}){{{body}}}", new {name, arguments, body}

It is not bad any more, only those indirect references wouldn't be there...

This is when string interpolation comes into the picture:

1 String res = $"function {name}({arguments}){{{body}}}");

It is just great. It directly provides strings, and arbitrary expression can be embedded in the placeholders. It is exactly what we need.

Customizing string interpolation

Previously I was not completely honest, string interpolation actually does not provide string directly, instead of an object of type;FormattableString. This can be stringified through its ToString method by a custom IFormatProvider.

When we want to execute our generators (a kind of state monad), we need to provide an initial state.

Thus all we have to do to use Generator<String> types expressions with string interpolation, is to use a stateful IFormatProvider:

1 class GFormatProvider : IFormatProvider, ICustomFormatter 2 { 3 private Context ctx; 4 5 public GFormatProvider(Context ctx) 6 { 7 this.ctx = ctx; 8 } 9 10 public object GetFormat(System.Type formatType) 11 { 12 if (formatType == typeof(ICustomFormatter)) return this; 13 return null; 14 } 15 public string Format(string format, object arg, IFormatProvider formatProvider) 16 { 17 if (arg == null) 18 return string.Empty; 19 20 // This is why we need the covariant type variable 21 if (arg is IGenerator<object>) 22 { 23 arg = ((IGenerator<object>)arg).Run(ctx).Item1; 24 } 25 26 if(arg is IFormattable) 27 { 28 return ((IFormattable)arg).ToString(format, formatProvider); 29 } 30 else 31 { 32 return arg.ToString(); 33 } 34 } 35 }

The provider is instantiated with a Context (the modified Context is dropped on purpose, but it could be returned if required). The Format method is executed for every placeholder, and it speaks for itself. It's worthwhile to note though that this is where we exploit that IGenerator<T> is covariant. Otherwise, an arbitrary IGenerator<T> couldn't be cast to IGenerator<object>

The only missing piece now is the utility method in Generator<T> to hide the custom IFormatProvider

1 public class Generator<T> : IGenerator<T> 2 { 3 4 ... 5 6 public static Generator<String> Template(FormattableString formattable) 7 { 8 return Wrap(delegate (Context ctx) 9 { 10 var provider = new GFormatProvider(ctx); 11 var res = formattable.ToString(provider); 12 return CovariantTuple<String, Context>.Create(res, ctx); 13 }); 14 } 15 }

Conclusion

With this straightforward extension to string interpolation, our prototype monadic generator/templating library is ready to make experiences. I hope its complexity does not hide the beauty in this monadic approach.

And finally, as usual, the completely source cod can be downloaded from the following GitHub repositoryhttps://github.com/tallcomponents/MonadicTemplate