TemplatedString feedback and extension - logging use-case, lazy values, blocks
Jim Laskey
james.laskey at oracle.com
Sat May 7 14:52:33 UTC 2022
[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