Yet another proposal of a templated string design

Remi Forax forax at univ-mlv.fr
Sat Oct 30 17:30:47 UTC 2021


Happy Halloween,
i spend some time again thinking how to implement templated strings.

>From the comments of my previous attempt, the main issues of the previous iteration were that it was a little too magical and by using a bootstrap method force the BSM design into the JLS.
So i try to borrow some aspects of the proposal from Brian and Jim and makes them mine.

I still think that Brian and Jim proposal to use an interface and to bind the values into the TemplatedString object far from good enough.

A templated string is fundamentally a string with holes (the template part) and some arguments (the sub expressions part),
so it should be modeled by a method call.

In a way, a templated string is similar to a varargs call, at the definition site, we want a special keyword like the symbol "..." for varargs and at call site the compiler do transformation some boxing / array creation in the case of varargs.
A templated string is in fact more like the opposite of varargs (a spread operator ?) because a templated string is expanded into several parameters, a constant TemplatedString and the values of the sub-expressions while the varargs collects several arguments into one parameter.

The other thing to remark is that the current syntax, something like Format."name: \(name) age: \(age)" omits the method name, so there is need for a convention for the compiler to linked a templated string call to an actual method definition in a similar way the name "value" is used when declaring an annotation without mentioning a method name.

I think we can group those two constraints by using that a method with a special name, i will use the hyphenated name "template-policy" in the rest of the document, obviously it can be any name. Using an hyphenated name has the advantage to be clear at definition site that the method is special and acts as a kind of spread operator.

So i propose that
  Format."name: \(name) age: \(age)"

is semantically equivalent to
  Format.template-policy(new TemplatedString("name: \uFFFC age: \uFFFC", ...), name, age).result()


You can notice that there is a call to result() on the returned value, it's because the returned value can be either a value or a value and a policy factory (a lambda to call to optimize the call, in a very similar way TemplatePolicy.asMethodHandle works).

So at declaration site the method template-policy looks like that

public class Format {
  public static TemplatePolicyResult<String> template-policy(TemplatedString templatedString, Object... args) {
    if (templatedString.parameters().size() != args.length) {
      throw new IllegalArgumentException(templatedString + " does not accept " + Arrays.toString(args));
    }
    var builder = new StringBuilder();
    for(var segment: templatedString.segments()) {
      builder.append(switch(segment) {
        case Text text -> text.text();
        case Parameter parameter -> args[parameter.index()];
      });
    }
    var text = builder.toString();
    return TemplatePolicyResult.result(text);
  }
}

The compiler can check that the expressions of the templated string are correctly typed, here they have to be assignable to Object.
The return type, is the type argument of TemplatePolicyResult<String>, so the result is a String.

If we want to optimize the template-policy to use the StringConcatFactory, instead of just specifying a result as return value,
we can also specify a policy factory.

public class Format {
  public static TemplatePolicyResult<String> template-policy(TemplatedString templatedString, Object... args) {
    ... // see above
    var text = builder.toString();
    return TemplatePolicyResult.resultAndPolicyFactory(text, StringConcat::policyFactory);
  }

  private static MethodHandle policyFactory(TemplatedString templatedString, MethodType methodType)
      throws StringConcatException {
    var recipe = templatedString.template().replace(TemplatedString.OBJECT_REPLACEMENT_CHARACTER, '\u0001');
    return StringConcatFactory.makeConcatWithConstants(MethodHandles.lookup(), "concat", methodType, recipe)
        .dynamicInvoker();
  }
}

The semantics is the following, the first time the method template-policy is called, if the result also comes with a policy factory,
all subsequent calls will use the method handle returned by the policy factory lambda.

Internally at runtime, it means using a MutableCallSite but with the guarantee that after one call the target will never change again
(and obviously there is no runtime check needed).

The runtime implementation is available here
  https://github.com/forax/java-interpolation/tree/master/policy-method/src/main/java/com/github/forax/policymethod

regards,
RĂ©mi


More information about the amber-spec-experts mailing list