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

Reply via email to