Are templated string embedded expressions "method parameters" or "lambdas"?

Tagir Valeev amaembo at gmail.com
Sat Oct 30 05:57:52 UTC 2021


Hello!

I think that deferred semantics could be confusing and it provides too
little benefit to justify its use. For example:

DebugLogger."The number of objects: \{counter.incrementAndGet()}";

We saw a number of bugs like this in assert statements. The unpleasant
thing is that in unit-tests assertions are usually turned on. It's
possible that debug logging is also turned on for tests. So we can
have successful tests and buggy production.

Also, it's unclear whether the template processor can invoke the
embedded expression several times, pass the MethodHandle to another
thread, invoke two embedded expressions concurrently. It's likely that
it can. In this case, the results could be totally unpredictable.

Another problem is the IDE refactoring. Taking the example above, we
cannot extract the variable without changing the semantics:

var count = counter.incrementAndGet();
DebugLogger."The number of objects: \{count}";

As static analysis is somewhat limited, IDE cannot always predict
whether the given template processor always executes the embedded
expression and whether the embedded expression has a side effect.
Thus, IDE cannot guarantee refactoring safety.

Finally, the inability to use non-effectively-final variables would be
very limiting. Note that the most classic way to iterate numbers in
Java is still for loop:

for(int i=0; i<array.length; i++) {
  System.out.println("array[\{i}] = \{array[i]}");
}

Inability to do this simple thing would be frustrating.

So to conclude, I think, in most cases, deferred execution provides
too little benefit but adds confusion, unnecessary limitations, and
overhead (more bytecode, more memory to store method handles, etc.)

I would somewhat agree with Remi that lazy embedded expression could
be a separate feature that should be used explicitly with separate
syntax, like:

DebugLogger."The number of objects: \{() ->
counter.incrementAndGet()}"; // adding `() -> ` I explicitly state
that I want side-effect to be executed only when logging is on.

In this case, block lambdas would also be possible, so I could extract
variable preserving the semantics:
DebugLogger."The number of objects: \{() -> {var count =
counter.incrementAndGet(); return count;}";

This lazy evaluation feature can be implemented as a separate
iteration, it's not necessary to deliver it together with the eager
evaluation one.

With best regards,
Tagir Valeev.

On Fri, Oct 29, 2021 at 9:11 PM Jim Laskey <james.laskey at oracle.com> wrote:
>
> For our early templated string prototypes, we restricted embedded expressions to just basic accessors and basic arithmetic. The intent was to keep things easy to read and to prevent side effects. Over time, we began thinking this restriction was unduly harsh. More precisely, we worried it that it would result in a complex, difficult-to-defend boundary. But we still would like users to not rely on side-effects.
>
> Consequently, a new proposal for embedded expressions - we would allow any Java expression with the restriction that you can't use single quotes, double quotes or escape sequences. We opted to keep this restriction to allow tools (ex., syntax highlighters) to isolate embedded expressions within strings without requiring sophisticated parsing.
>
> Given that an unprocessed templated string involves at least some deferred evaluation, should we frame templated string parameters as being more like method parameters (all parameters evaluated eagerly, left to right), or should we treat them as lambda expressions, which may capture (effectively final) variables from the environment, and evaluate the full parameters expressions when they are needed?
>
> Note too that the effectively final restriction rules out some of the worst side-effect offenders, like:
>
>     int x = 0;
>     formatter."One \{x++} plus two \{x++} is three \{x}";
>
> -- even if we intend to then do eager evaluation!
>
> To help understand the issue, let's look at a simplification of how the two different paradigms (method parameter vs. lambda) might be implemented. Example:
>
>     int x = 0;
>
>     int method1() {
>         System.out.println("one");
>         return 1;
>     }
>
>     int method2() {
>         System.out.println("two");
>         return 2;
>     }
>
>     System.out.println("Before TemplatedString");
>     TemplatedString ts = "\{x} and \{method1()} and \{method2()}";
>     System.out.println("After TemplatedString");
>     System.out.println(CONCAT.apply(ts));
>     System.out.println("After Policy");
>
> The method parameter paradigm would generate something like following for TemplatedString ts = "\{x} and \{method1()} and \{method2()}"; statement. Basically, capture the values of the evaluated expressions in instance fields.
>
>     TemplatedString ts = new TemplatedString() {
>         int expr$0 = x;
>         int expr$1 = method1();
>         int expr$2 = method2();
>
>         String template() {
>             return "\uFFFC and \uFFFC and \uFFFC";
>         }
>
>         List<Object> values() {
>             return List.of(expr$0, expr$1, expr$2);
>         }
>
>         String concat() {
>             return expr$0 + " and " + expr$1 + " and " + expr$2;
>         }
>
>         List<MethodHandle> vars() {
>             return List.of(lookupGetter("expr$0"), lookupGetter("expr$1"), lookupGetter("expr$2"));
>         }
>     }
>
> The lambda paradigm would generate something like following. Basically, wrap the expression in an instance method and capturing effectively final values used by the methods in instance fields (ala lambda.)
>
>     TemplatedString ts = new TemplatedString() {
> int var$x = x;
>
>         int expr$0() {
>             return var$x;
>         }
>
>         int expr$1() {
>             return method1();
>         }
>
>         int expr$2() {
>             return method2();
>         }
>
>         String template() {
>             return "\uFFFC and \uFFFC and \uFFFC";
>         }
>
>         List<Object> values() {
>             return List.of(expr$0(), expr$1(), expr$2());
>         }
>
>         String concat() {
>             return expr$0() + " and " + expr$1() + " and " + expr$2();
>         }
>
>         List<MethodHandle> vars() {
>             return List.of(lookupMethod("expr$0"), lookupMethod("expr$1"), lookupMethod("expr$2"));
>         }
>     }
>
> The output from the method parameter paradigm would be:
>
>     Before TemplatedString
>     one
>     two
>     After TemplatedString
>     0 and 1 and 2
>     After Policy
>
> From the lambda paradigm would be:
>
>     Before TemplatedString
>     After TemplatedString
>     one
>     two
>     0 and 1 and 2
>     After Policy
>
> To help us evaluating the tradeoffs between the two paradigms, our question to the experts is, "What are the ramifications of each?" Please resist the temptation to express a preference for one or the other.
>
> Thank you.
>
> Cheers,
>
> -- Jim


More information about the amber-spec-experts mailing list