TemplatedString feedback and extension - logging use-case, lazy values, blocks

Attila Kelemen attila.kelemen85 at gmail.com
Thu May 12 18:07:33 UTC 2022


It seems I did too much extrapolation after sloppy reading.
Sorry for that.

Anyway, this seems nice. Just one thing (which is partially mentioned
in the alternatives section of the JEP). The syntax looks a bit
bizarre to me (even though it isn't ambiguous), because this adds an
additional conceptual weight to the language given that it looks like
as if the templated string instance is the method name.

In fact, the syntax `MY_POLICY."\{x};\{y}"` could be moved out from
this JEP, and simply provide a method both ways:

- `TemplatedString.format(TemplatePolicy)`
- `TemplatePolicy.apply(TemplatedString)` (already in JEP).

If we ever needed the JEP provided sugar, then I would try with
something more generic that would benefit other things as well
consistently, not just special case string templates. Since as I can
see the main benefit of the syntax is that we don't need to bother
with parenthesis.

But otherwise, this feature would be indeed very useful. Though,
I can already see people abusing a TO_STRING policy (which will
surely be provided in commons-lang or similar, if not the JDK) :)

Attila

Jim Laskey <james.laskey at oracle.com> ezt írta (időpont: 2022. máj.
12., Cs, 19:25):
>
> [Note that all code samples below are speculative and subject to change. There is also an assumption reader has read and has an understanding of the JEP draft. https://openjdk.java.net/jeps/8273943]
>
> I've had a chance to isolate some examples to demonstrate an approach for deferred evaluation using suppliers.
>
> Starting with the general pattern for a (string result) policy as follows;
>
> static final StringPolicy MY_SP = ts -> {
>     StringBuilder sb = new StringBuilder();
>     Iterator<String> fragmentsIter = ts.fragments().iterator();
>     for (Object value : ts.values()) {
>         sb.append(fragmentsIter.next());
>         sb.append(value);
>     }
>     sb.append(fragmentsIter.next());
>     return sb.toString();
> };
>
> ...
>
> int x = 10, y = 20;
> String result = MY_SP."\{x} + \{y} = \{x + y}"; // The result will be "10 + 20 = 30".
>
> To introduce deferred evaluation we can add a test for java.util.function.Supplier and
> use the supplier's value instead;
>
> static final StringPolicy MY_SP = ts -> {
>     StringBuilder sb = new StringBuilder();
>     Iterator<String> fragmentsIter = ts.fragments().iterator();
>     for (Object value : ts.values()) {
>         sb.append(fragmentsIter.next());
>         //------------------------------------------
>         if (value instanceof Supplier<?> supplier) {
>             sb.append(supplier.get());
>         } else {
>             sb.append(value);
>         }
>         //------------------------------------------
>     }
>     sb.append(fragmentsIter.next());
>     return sb.toString();
> };
>
> ...
>
> static final DateTimeFormatter HH_MM_SS = DateTimeFormatter.ofPattern("HH:mm:ss");
> static final Supplier<String> TIME_STAMP = () -> LocalDateTime.now().toLocalTime().format(HH_MM_SS);
>
> ...
>
> String message = MY_SP."\{TIME_STAMP}: Received response"; // The result will be "13:56:48: Received response".
>
> In this case, the formatting of the time stamp is deferred until the policy is applied. This example can be expanded to test for
> java.util.concurrent.Future and java.util.concurrent.FutureTask. The advantage of futures
> over Supplier is that they are only evaluated once;
>
> static final StringPolicy MY_SP = ts -> {
>     StringBuilder sb = new StringBuilder();
>     Iterator<String> fragmentsIter = ts.fragments().iterator();
>     for (Object value : ts.values()) {
>         sb.append(fragmentsIter.next());
>         //------------------------------------------
>         if (value instanceof Future future) {
>             if (future instanceof FutureTask task) {
>                 task.run();
>             }
>
>             try {
>                 sb.append(future.get());
>             } catch (InterruptedException | ExecutionException e) {
>                 throw new RuntimeException(e);
>             }
>         } else if (value instanceof Supplier<?> supplier) {
>             sb.append(supplier.get());
>         } else {
>             sb.append(value);
>         }
>         //------------------------------------------
>     }
>     sb.append(fragmentsIter.next());
>     return sb.toString();
> };
>
>
> To answer Attila's comments. 1) Nothing is computed unless the policy performs an action, so a logging policy
> that has an if(ENABLED) around it's main code and static final ENABLED = false; will have little or no overhead.
> 2) One of the primary goals of template policies is to screen inputs to prevent injection attacks. So a well written policy should not be
> any more susceptible that a println statement. There is no interpretation or a need for interpretation in string templates. Policies may
> add interpretation, but then the onus is on the policy
>
> Cheers,
>
> -- Jim
>
>
>
> On May 7, 2022, at 11:52 AM, Jim Laskey <james.laskey at oracle.com> wrote:
>
> [OOTO]
>
> I’ve already done  examples of all of the items in your list (all good), but i’m glad you brought these topics up for open discussion. I’ll sit back a see what others have to say.
>
> However, I would like to point out that deferred evaluation requires an agreement from the policy to special case suppliers (or futures). This is something we don’t really want to bake in. Every policy should be free to pick and choose what it does.
>
> Another approach is to use policy chaining where one policy in the chain evaluates suppliers and futures to produce a new TemplatedString to be processed by the next policy in the chain.
>
> The ugliness of lambdas in an embedded expression is made worse by the fact it has to be cast to Supplier<T> or Future<T> since the default type for embedded expressions is Object. In all my examples, I’ve out of lined these types of expressions and used a local or better yet a global constant variable (ex., TIME_STAMP) to use in the embedded expression. I think this technique makes the string template read better.
>
> When I’m back the end of next week I can post some examples.
>
> Cheers,
>
> —Jim
>
> After this response we should move the discussion to the amber-dev mailing list.
>
> ��
>
> On May 7, 2022, at 10:58 AM, Adam Juraszek <juriad at gmail.com> wrote:
>
> The TemplatedString proposal is amazing; I can see using it in many places.
> I have a few comments that I have not seen addressed.
>
> Logging
> =======
>
> One place where some kind of string templating is often used is logging.
> Most logging frameworks use a pair of braces {}. TemplatedStrings have the
> potential to revolutionize it.
>
> Log record usually consists of the following parts that the programmer
> needs to provide:
> * log level (such as error, info)
> * message (such as "Oh no, the value {} is negative")
> * parameters (such as -42)
> * exception/throwable (only sometimes)
>
> Let's assume that TemplatedString is performant and virtually free, then
> these options would be possible. I cannot say which syntax the logging
> frameworks would choose:
>
> logger.error."Oh no, the value \{value} is negative\{ex}";
> logger.error("Oh no, the value \{value} is negative", ex);
> logger."Oh no, the value \{value} is negative".error(ex);
>
> Versus the existing:
>
> logger.error("Oh no, the value {} is negative", value, ex);
>
> Lazy Logging
> ============
>
> Some logging frameworks such as Log4j2 allow lazy evaluation for example:
>
> logger.trace("Number is {}", () -> getRandomNumber());
>
> I must admit that doing the same using TemplatedStrings is ugly and the 6
> distinct characters introducing a lazily evaluated value are impractical:
>
> logger.trace("Number is \{() -> getRandomNumber()}");
>
> Can we offer something better using TemplatedStrings? The JEP already talks
> about evaluation. For example, this is valid:
>
> int index;
> String data = STR."\{index=0}, \{index++}";
>
> and mentions that the TemplatedString provides a list of values:
>
> TemplatedString ts = "\{x} plus \{y} equals \{x + y}";
> List<Object> values = ts.values();
>
> I want to propose an additional syntax. The escape sequence \( would denote
> a lazily computed value - probably stored as a Supplier. Because the value
> may not be evaluated, it must only refer to effectively final variables
> (the same rules as for lambda expressions apply).
>
> The following should be illegal:
>
> int index;
> String data = STR."\(index=0), \(index++)";
>
> The logging use-case could be expressed more naturally:
>
> logger.trace("Number is \(getRandomNumber())");
>
> I personally can see a parallel between \(...) and ()-> as both use
> parentheses. This unfortunately is the exact opposite of the syntax of
> closures in Groovy, where {123} is "lazy" and (123) produces the value
> immediately. Different languages, different choices.
>
> When should this lazy expression be evaluated, and who is responsible for
> it? It can either be:
> * when ts.values() is called;
> * when the value is accessed (via ts.get(N) or .next() on the List's
> Iterator);
> * manually when it is consumed by the author of the policy using an
> instanceof check for Supplier.
>
> I keep this as an open question (first we need to find out if this is even
> a useful feature). It also remains open what to do when a user provides a
> Supplier manually:
>
> Supplier s = () -> 123
> logger.trace("Number is \{s} \(s)");
>
> Another way to express a Supplier is via a method reference, which should
> also be supported and would suggest the equivalence of the following:
>
> logger.trace("Number is \{this::getRandomNumber}");
> logger.trace("Number is \(this.getRandomNumber())");
>
> Blocks
> ======
>
> What if the computation of the value requires multiple statements?
>
> STR."The intersection of \{a} and \{b} is \{((Supplier) ()-> {var c = new
> HashSet<>(a); c.retainAll(b); return c;}).get()}"
> STR."The intersection of \{a} and \{b} is \{switch(1){default:var c = new
> HashSet<>(a); c.retainAll(b); yield c;}}"
>
> These are (as far as my imagination goes) the easiest ways to turn a block
> into an expression.
>
> What if we introduced a special syntax for blocks? One option would be
> simply doubling the braces:
>
> STR."The intersection of \{a} and \{b} is \{{var c = new HashSet<>(a);
> c.retainAll(b); yield c;}}"
>
> The last closing brace is probably not needed but keeping them balanced
> will help dumb editors with highlighting.
>
> This could be a more general syntax useful even outside TemplatedStrings
> (especially in final field initialization):
>
> var intersection = {var c = new HashSet<>(a); c.retainAll(b); yield c;};
>
> Lazy Blocks
> ===========
>
> We may want to combine the two proposed features and compute the
> intersection lazily when logging:
>
> logger.trace("The intersection of \{a} and \{b} is \({var c = new
> HashSet<>(a); c.retainAll(b); return c;})")
>
> which would be equivalent to this already supported syntax provided that
> the policy understands Supplier values:
>
> logger.trace("The intersection of \{a} and \{b} is \{() -> {var c = new
> HashSet<>(a); c.retainAll(b); return c;}}")
>
> If we introduce \( as described in Lazy Logging above and say that
> \(expression) is equivalent to \{() -> expression}, then \({statements})
> would naturally be equivalent to \{() -> {statements}}.
>
> Literals
> ========
>
> It was already mentioned in the JEP that it is possible to create a policy
> returning a JSON object. We can also think of them as a way to express
> literals. Some Java types already have a way to parse a String. Some
> examples are:
>
> DURATION."PT20.345S"
> Duration.from("PT20.345S")
>
> COMPLEX."1+3i"
> Complex.from(1, 3)
>
> US_DATE."7/4/2022"
> LocalDate.of(2022, 7, 4)
>
> This is almost as powerful as what C++ offers (
> https://en.cppreference.com/w/cpp/language/user_literal).
>
> Conclusion
> ==========
>
> I am very happy with the current proposal and tried to explore some
> possible extensions. I proposed a syntax for lazy evaluation of values,
> blocks as values, and a combination thereof. I would like to hear from you,
> whether this is something worth considering, and whether there is demand
> for it.
>
> Regards
> Adam
>
>


More information about the amber-dev mailing list