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

Adam Juraszek juriad at gmail.com
Sat May 7 13:58:24 UTC 2022


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