New drop in templated strings branch

Jim Laskey james.laskey at oracle.com
Mon Dec 6 17:23:01 UTC 2021


A new templated strings drop is available in the amber repo<https://github.com/openjdk/amber.git> (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<Object> values = templatedString.values();
     *
     * values will be equivalent to List.of(a, b, a + b)
     *
     * @return list of expression values
     */
    List<Object> 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<String> 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<String> 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<String> 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 <R>  Policy's apply result type.
     * @param <E>  Exception thrown type.
     *
     * @return constructed object of type R
     *
     * @throws E exception thrown by the template policy when validation fails
     */
    default <R, E extends Throwable> R apply(TemplatePolicy<R, E> 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<String> 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<R, E extends Throwable> {

    /**
     * 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 <R>  Type of the function's result.
     *
     * @return a TemplatePolicy that applies the function's template policy
     */
    public static <R> TemplatePolicy<R, RuntimeException>
            ofComposed(BiFunction<List<String>, List<Object>, 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 <R>  Type of the function's result.
     *
     * @return a TemplatePolicy that applies the function's template policy
     */
    public static <R> TemplatePolicy<R, RuntimeException>
            ofTransformed(Function<String, R> 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<String, RuntimeException> 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<String, RuntimeException> {

    /**
     * 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<String, RuntimeException> {
        @Override
        public String apply(TemplatedString templatedString) {
            StringBuilder sb = new StringBuilder();
            Iterator<String> segmentsIter = templatedString.segments().iterator();
            List<Object> 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<String, RuntimeException> simple =
        TemplatePolicy.ofComposed((segments, values) -> {
            StringBuilder sb = new StringBuilder();
            Iterator<String> 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"



More information about the amber-spec-observers mailing list