Skip to content

Instantly share code, notes, and snippets.

@jaredpar
Last active March 15, 2021 18:23
Show Gist options
  • Select an option

  • Save jaredpar/8fde9c8edc1b541d8b0e5fd5bb0bf23d to your computer and use it in GitHub Desktop.

Select an option

Save jaredpar/8fde9c8edc1b541d8b0e5fd5bb0bf23d to your computer and use it in GitHub Desktop.

The key items we need to accomplish is

  1. Find the builder type
  2. Pass user arguments (Span, culture, etc ...) to builder ctor
  3. Pass compiler arguments (hole count, fixed length) to the builder ctor
  4. Allow for non-string results
  5. Binary compatibility: let existing code evolve without breaking existing callers.

Consider if we allowed developers to define methods like the following

[InterpolatedBuilder(typeof(ValueStringBuilder))]
static extern string Format(Span<byte> span, string format, params object[] args);

Now at the call site the way we handle this is if we see the [InterpolatedBuilder] attribute we do the following. Take all of the arguments before format (first N-2 arguments) and pass them along with the compiler arguments to the constructor of ValueStringBuilder

return Format(mySpan, $"Hello {name}");
// becomes
var builder = new ValueStringBuilder(mySpan, baseLength: 9, holeCount: 1);
builder.TryFormat(name);
return builder.GetResult();

This makes passing arguments natural and visible at the call site. It also doesn't required name based mapping it's just simply positional based mapping like we've done in say LINQ.

This also extends to an aribtary number of arguments. Essentially the pattern is to pass all of the arguments up until format to the constructor of the builder.

[InterpolatedBuilder(typeof(ValueStringBuilder))]
static extern string Format2(Type1 param2, Type2 param2, ... TypeN paramN, string format, object[] args);

// At the callsite
Format2(arg1, arg2, ... argN, $"Hello {name}");
// Becomes
var builder = new ValueStringBuilder(arg1, arg2, ... argN, baseLength: 9, holeCount: 1);
builder.TryFormat(name);
return builder.GetResult();

For APIs that want to move to new builders but maintain binary compatibility could do so by defining the method as non-extern and defering to their existing manner of formatting strings

// V1 version of API 
static string Log(string format, params object[] args) => string.Format(foramt, args); 

// V2 new callers use ValueStringBuilder but existing callers go through old path
[InterpolatedBuilder(typeof(ValueStringBuilder))]
static string Log(string format, params object[] args) => string.Format(foramt, args); 

This allows allows for us to have non-string return types. The compiler would enforce that the return type of GetResult() on the builder matched the return type of the method where the attribute was placed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment