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

Attila Kelemen attila.kelemen85 at gmail.com
Sat May 7 15:39:40 UTC 2022


Hi,

Just giving my two cents :)

Logging: While of course you could use such a feature in a log message.
However, that wouldn't be so good for two reasons:
1. It would not prevent the string concatenation in case of no logging, which
is one reason why these things were invented in the first place.
2. If a logging framework interprets the log message (before resolving
the template),
then that would be like an SQL injection.

Blocks: In my opinion, blocks of code kinda defeats the purpose of
string interpolation
to make string concatenation look more readable. In fact, I have created my own
implementation (https://github.com/kelemen/jdk/tree/string_interpolation)
of string
interpolation about two years ago (though only rebased my code to a one year old
state of the JDK), and in this implementation I even explicitly
disallowed line breaks
in these expressions. Mainly, because it raises questions in text blocks.

Lazy evaluation: I think this could only work, if we had a new type
like GString in Groovy,
because otherwise the lambda expression has to be evaluated
immediately anyway, since
the type is `java.lang.String` which can't be expected to carry around
the lazy expressions.

Attila


Adam Juraszek <juriad at gmail.com> ezt írta (időpont: 2022. máj. 7., Szo, 15:58):
>
> 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 jdk-dev mailing list