From james.laskey at oracle.com Mon Dec 6 17:23:01 2021 From: james.laskey at oracle.com (Jim Laskey) Date: Mon, 6 Dec 2021 17:23:01 +0000 Subject: New drop in templated strings branch Message-ID: <428CAF23-E6F1-4792-B6C4-2967BA236CCB@oracle.com> A new templated strings drop is available in the amber repo (branch templated-strings). Note that this is still an early prototype - don?t bank on any jdk modifications found in the branch. Changes A few significant changes have occurred since the last drop: * The supplied policies STR and FMT are renamed CONCAT and FORMAT. * RuntimeException is being used as the default exception type in pre-supplied policies (was Throwable). * All expression restrictions found in earlier drops have been lifted. However, style guides will discourage the use of large multi-line expressions. * Anonymous classes are no longer used to capture expression values. There was concern that significant templated string use would proliferate many small, often with similar layout, classes. We opted to develop a carrier object (java.lang.runtime.Carrier) which can be reused when layouts are similar but still avoid boxing. * The limit to the number of expressions that can occur in a templated string has increased beyond the maximum number of method arguments (255) by using an array based carrier when the number exceeds 255 (actually 253). * To facilitate performance work, specifically for formatted templated strings, there have been some additions to StringConcatFactory. This work will likely migrate to a separate internal location in the future - don?t rely on those methods being public in the future. * The optimized (BSM) form of TemplatePolicy has been excised into a sealed interface. There is a concern that developers will start using templated strings as a methodology for constant expressions or lazy initialization. This is not the droid you are looking for. Currently, CONCAT (ConcatPolicy) and FORMAT (FormatterPolicy) are the only implementations. * Templated string expressions are evaluated at instantiation, left to right. We had some discussion about deferred evaluation, but the consensus was that lambda expressions can be used to provision deferment. * Added an experimental templated string builder class to assist with the concatenation of strings and templated strings. * Added a TemplatedString.of(string) method to create TemplatedStrings from simple strings. Top Level APIs TemplatedString /** * A TemplatedString object represents the information captured from a templated string * expression. This information is comprised of a template string and the values from the * evaluation of embedded expressions. The template string is a string with placeholders * where expressions existed in the original templated string expression. */ public interface TemplatedString { /** * Placeholder character marking insertion points in a templated string. The value used is * the unicode OBJECT REPLACEMENT CHARACTER. */ public static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC'; /** * String equivalent of OBJECT_REPLACEMENT_CHARACTER. */ public static final String OBJECT_REPLACEMENT_CHARACTER_STRING = Character.toString(OBJECT_REPLACEMENT_CHARACTER); /** * Returns the template string with placeholders. In the example: * * TemplatedString templatedString = "\{a} + \{b} = \{a + b}"; * String template = templatedString.template(); * * template will be equivalent to \uFFFC + \uFFFC = \uFFFC". * * @return template string with placeholders. */ String template(); /** * Returns an immutable list of expression values. In the example: *: * TemplatedString templatedString = "\{a} + \{b} = \{a + b}"; * List values = templatedString.values(); * * values will be equivalent to List.of(a, b, a + b) * * @return list of expression values */ List values(); /** * Returns an immutable list of string segments created by splitting the template string * at placeholders using TemplatedString.split(template). In the example: * * TemplatedString templatedString = "The student \{student} is in \{teacher}'s class room."; * List segments = templatedString.segments(); * * segments will be equivalent to List.of("The student ", " is in ", "'s class room.") * * segments() is a convenience method for TemplatePolicies that * construct results using StringBuilder. Typically using the following pattern: * * StringBuilder sb = new StringBuilder(); * Iterator segmentsIter = templatedString.segments().iterator(); * * for (Object value : templatedString.values()) { * sb.append(segmentsIter.next()); * sb.append(value); * } * * sb.append(segmentsIter.next()); * String result = sb.toString(); * * @implSpec The list returned is immutable. * * @implNote The TemplatedString implementation generated by the compiler for a * templated string expression guarantees efficiency by only computing the segments list once. * Other implementations should make an effort to do the same. * * @return list of string segments */ default List segments() { return TemplatedString.split(template()); } /** * Returns the template string with the values inserted at placeholders. * * @return the template string with the values inserted at placeholders */ default String concat() { return TemplatedString.concat(this); } /** * Returns the result of applying the specified policy to this TemplatedString. * * @param policy the TemplatePolicy instance to apply * * @param Policy's apply result type. * @param Exception thrown type. * * @return constructed object of type R * * @throws E exception thrown by the template policy when validation fails */ default R apply(TemplatePolicy policy) throws E { return policy.apply(this); } /** * Produces a diagnostic string representing the supplied TemplatedString. * * @param templatedString the TemplatedString to represent * * @return diagnostic string representing the supplied templated string * * @throws NullPointerException if templatedString is null */ public static String toString(TemplatedString templatedString) { ... } /** * Splits a string at placeholders, returning an immutable list of strings. * * @implSpec Unlike String.split() this method returns an * immutable list and retains empty edge cases (empty string and string * ending in placeholder) so that segments prefix and suffix all placeholders * (expression values). That is, segments().size() == values().size() * + 1. * * @param string a string with placeholders * * @return list of string segments * * @throws NullPointerException if string is null */ public static List split(String string) { ... } /** * Returns the template with the values inserted at placeholders. * * @param templatedString the TemplatedString to concatenate * * @return the template with the values inserted at placeholders * * @throws NullPointerException if templatedString is null */ public static String concat(TemplatedString templatedString) { ... } /** * Returns a TemplatedString composed from a String. * * @implSpec The string can not contain expressions or OBJECT REPLACEMENT CHARACTER. * * @param string a string to be composed into a TemplatedString. * * @return TemplatedString composed from string * * @throws IllegalArgumentException if string contains OBJECT REPLACEMENT CHARACTER. * @throws NullPointerException if string is null */ public static TemplatedString of(String string) { ... } /** * Constructs a new TemplatedString.Builder. * * @return a new TemplatedString.Builder */ public static Builder builder() { return new Builder(); } ... } Template policy /** * This interface describes the methods provided by a templated string policy. The primary * method TemplatePolicy.apply() is used to validate and compose a result using * the template string and list of values, from a TemplatedString. */ public interface TemplatePolicy { /** * Constructs a result based on the template string and values in the supplied * templatedString object. * * @param templatedString a TemplatedString instance * * @return constructed object of type R * * @throws E exception thrown by the template policy when validation fails */ R apply(TemplatedString templatedString) throws E; /** * Produces a template policy based on a supplied bi-function (lambda). The * function's inputs will be a the list of segments and a list of values * from the TemplatedString object. The result type from the * function will be the result type of the generated policy. * * @param policy function for applying template policy * * @param Type of the function's result. * * @return a TemplatePolicy that applies the function's template policy */ public static TemplatePolicy ofComposed(BiFunction, List, R> policy) { return new TemplatePolicy<>() { @Override public final R apply(TemplatedString templatedString) { Objects.requireNonNull(templatedString); return policy.apply(templatedString.segments(), templatedString.values()); } }; } /** * Produces a template policy based on a supplied function (lambda). The * function's input will be the basic concatenation, * TemplatedString.concat(), from the TemplatedString object. The * result type from the function will be the result type of the generated * policy. * * @param policy function for applying template policy * * @param Type of the function's result. * * @return a TemplatePolicy that applies the function's template policy */ public static TemplatePolicy ofTransformed(Function policy) { return new TemplatePolicy<>() { @Override public final R apply(TemplatedString templatedString) { Objects.requireNonNull(templatedString); return policy.apply(templatedString.concat()); } }; } /** * Simple concatenation policy instance. */ public static final TemplatePolicy CONCAT = new ConcatPolicy(); ... } FormatterPolicy /** * This TemplatePolicy constructs a String result using Formatter. * Unlike Formatter, FormatterPolicy locates values in the expressions that * come immediately after the format specifier. TemplatedString expressions * without a preceeding specifier, use "%s" by default. */ public final class FormatterPolicy implements Linkage { /** * Predefined FormatterPolicy instance that uses default locale. */ public static final FormatterPolicy FORMAT = new FormatterPolicy(); /** * Locale used by this FormatterPolicy. */ private final Locale locale; /** * Constructor. */ public FormatterPolicy() { this.locale = null; } /** * Constructor. * * @param locale formatting locale */ public FormatterPolicy(Locale locale) { this.locale = locale; } /** * Returns the {@link FormatterPolicy} instance locale. * * @return the {@link FormatterPolicy} instance locale */ public Locale locale() { return locale; } @Override public final String apply(TemplatedString templatedString) { ... } ... } TemplatedString.Builder /** * This class can be used to construct a new TemplatedString from string * segments, values and other TemplatedStrings. * * To use, construct a new Builder using TemplatedString.builder(), * then chain invokes of builder.segment(string) or builder.value(object) * to build up the template and values. builder.templatedString(templatedString) can be * used to add the template and values from another TemplatedString. * builder#build() can be invoked at the end of the chain to produce a new * TemplatedString using the current state of the builder. * * Example: * * int x = 10; * int y = 20; * TemplatedString ts = TemplatedString.builder() * .segment("The result of adding ") * .value(x) * .templatedString(" and \{y} equals \{x + y}") * .build(); * String result = CONCAT.apply(ts); * * Result: "The result of adding 10 and 20 equals 30" * * The Builder itself implements TemplatedString. When * applied to a policy will use the current state of template and values to * produce a result. */ public static class Builder implements TemplatedString { ... /** * Add the supplied TemplatedString's template and * values to the builder. * * @param templatedString existing TemplatedString * * @return this Builder * * @throws NullPointerException if templatedString is null */ public Builder templatedString(TemplatedString templatedString) { ... } /** * Add a string segment to the Builder's template. * * @param string string segment to be added * * @return this Builder * * @throws IllegalArgumentException if segment contains OBJECT REPLACEMENT CHARACTER * @throws NullPointerException if string is null */ public Builder segment(String string) { ... } /** * Add a value to the Builder. This method will also insert a placeholder * in the Builder's template. * * @param value value to be added * * @return this Builder * * @throws NullPointerException if value is null */ public Builder value(Object value) { ... } /** * Returns a TemplatedString based on the current state of the * {@link Builder Builder's} template and values. * * @return a new TemplatedString */ public TemplatedString build() { ... } } Examples byte a = 10; byte b = 20; // Using the optimized ConcatPolicy System.out.println(CONCAT."The answer is \{a} + \{b} = \{a + b}"); // Using the TemplatedString.concat() method System.out.println("The answer is \{a} + \{b} = \{a + b}".concat()); // Using the optimized FormatterPolicy System.out.println(FORMAT."The answer is %5d\{a} + %5d\{b} = %5d\{a + b}"); // Using the PrintStream.format(TemplatedString) overloaded method System.out.format("The answer is %5d\{a} + %5d\{b} = %5d\{a + b}"); // Define a simple policy class SimplePolicy implements TemplatePolicy { @Override public String apply(TemplatedString templatedString) { StringBuilder sb = new StringBuilder(); Iterator segmentsIter = templatedString.segments().iterator(); List values = templatedString.values(); for (Object value : values) { sb.append(segmentsIter.next()); sb.append(value); } sb.append(segmentsIter.next()); return sb.toString(); } } // New instance of policy (can be reused and is Thread safe) SimplePolicy simple = new SimplePolicy(); // Using simple policy directly System.out.println(simple."The answer is \{a} + \{b} = \{a + b}"); // Applying simple policy to templated string System.out.println("The answer is \{a} + \{b} = \{a + b}".apply(simple)); // Alternate way to implement simple policy TemplatePolicy simple = TemplatePolicy.ofComposed((segments, values) -> { StringBuilder sb = new StringBuilder(); Iterator segmentsIter = segments.iterator(); for (Object value : values) { sb.append(segmentsIter.next()); sb.append(value); } sb.append(segmentsIter.next()); return sb.toString(); }); // Simpler still var simple = TemplatePolicy.ofTransformed(Function.identity()); // TemplatedString Builder example. int x = 10; int y = 20; TemplatedString ts = TemplatedString.builder() .segment("The result of adding ") .value(x) .templatedString(" and \{y} equals \{x + y}") .build(); String result = CONCAT.apply(ts); Result: "The result of adding 10 and 20 equals 30"