Partial Expression Reduction

The FreshMarker template engine has the Partial Template Reduction feature. This makes it possible to create a new Template from an existing Template by adding an incomplete model. Some constructs are simplified in the new Template because the data from the incomplete model could be evaluated there. Until now, this could only be used in directives and interpolations, so why not use the mechanism in expressions as well?

It is not entirely correct that the mechanism has not yet been used in expressions. However, the all or nothing law has applied here up to now. If an expression could be evaluated, then its result was used in a directive or interpolation for simplification.

<#if price * amount < free_limit>
Limit not reached
<#else>
Limit reached
</#if>

In this simple example, the If Directive can be simplified if all three variables price, amount and free_limit are known. In this case, the Template is simplified by only containing the text Limit not reached or Limit reached. If one of the variables is not known, the If Directive is not yet simplified.

However, it would be nice if there was also a simplification if, for example, only price and amount are known.

<#if 42 < free_limit>
Limit not reached
<#else>
Limit reached
</#if>

In this example, the values 6 and 7 were present in the model and a simplification to the expression 42 < free_limit was possible.

To make the simplifications, the implementations of TemplateObject all receive a reduce method. The contract for this method stipulates that it either returns a simplified form of the sub-expression that begins with this object or the object itself.

public interface TemplateObject {
  default TemplateObject reduce(ReduceContext context) {
    return this;
  }
}

The method is defined as a default method in the TemplateObject interface and therefore does not have to be implemented for all primitive model types. The simplification of an instance of TemplateString is the instance itself and the simplification of a TemplateNumber instance is the instance itself.

To gain initial experience with the new feature, we add it to the interpolation. To do this, we extend the reduce method of the InterpolationFragment class.

@Override
public Fragment reduce(ReduceContext context) {
  TemplateMarkup reduced = partialExpressionReduction ? expression.reduce(context) : expression;
  try {
    TemplateString templateObject = reduced.evaluate(context, TemplateString.class);
    context.getStatus().replaced().incrementAndGet();
    return new ConstantFragment(templateObject.getValue());
  } catch (WrongTypeException e) {
    throw new ReduceException(e.getMessage(), ftl, e);
  } catch (ProcessException e) {
    if (partialExpressionReduction && reduced != expression) {
      context.getStatus().expressions().add(expression);
      context.getStatus().expressions().add(reduced);
      log.debug("Reduced: {} to {}", expression, reduced);
      return new InterpolationFragment(reduced, ftl, true);
    }
    return this;
  }
}

In the first line, a simplified instance of the expression is determined and then checked to see whether the InterpolationFragment instance can be replaced by a ConstantFragment instance. This part of the implementation has remained almost the same. If a ProcessException is thrown in the reduce method, then the InterpolationFragment instance is not simply returned, but a new one if necessary.

If the feature is activated and we have another instance of TemplateMarkup in reduced, then we create a new InterpolationFragment instance with the simplified expression. In addition, we remember the expression before and after the simplification in the ReductionStatus. This is the only way we can recognize changes within the template and check them in the unit tests.

The previous implementations do not yet provide any simplifications because all TemplateObject implementations return this as the result of the reduce method. For most of the leaves in our expression trees the behavior is already suitable, now we have to adapt the most important reduce methods from the root of the expression.

First the TemplateMarkup class, which is always the root of the expression in InterpolationFragment instances.

@Override
public TemplateMarkup reduce(ReduceContext context) {
  TemplateObject templateObject = content.reduce(context);
  if (templateObject.isNull() || templateObject == content) {
    return this;
  }
  if (templateObject.isPrimitive()) {
    return new TemplateMarkup(getString(context, templateObject));
  }
  return new TemplateMarkup(templateObject);
}

The content is reduced in the first line. If the result is NULL or no simplification could be made, the current instance is returned without changes, otherwise it is checked whether it is a primitive model type. In this case, a String is created and a new TemplateMarkup instance is returned. This also means that a ConstantFragment instance is created from the String, which replaces the current InterpolationFragment. In the other case, a new TemplateMarkup instance is created with the simplified expression.

The ModelVariable class provides the variables from the model and also requires its own reduce implementation.

@Override
public TemplateObject reduce(ReduceContext context) {
  TemplateObject value = evaluateToObject(context);
  if (value == TemplateNull.NULL) {
    return this;
  }
  context.getStatus().expression().incrementAndGet();
  return value;
}

If the desired variable is not contained in the model, the implementation returns its own instance; otherwise, it replaces its own instance with the value from the model.

The previous implementations all have something in common, only with them the feature cannot be implemented, because they either return an expression that has been completely reduced because only filled ModelVariable instances or primitive model types were involved or an expression that has not been reduced because other participants have returned this. These other participants are inner nodes of the expression tree such as TemplateOperation, TemplateJunction, TemplateRelation or TemplateNot.

This also means that all expressions can only be reduced if all inner nodes receive an appropriate implementation. As long as this has not been realized, the feature remains experimental.

Fortunately, some inner nodes have already been adapted. Here is the implementation for TemplateRelational.

@Override
public TemplateObject reduce(ReduceContext context) {
  TemplateObject leftObject = left.reduce(context);
  TemplateObject rightObject = right.reduce(context);
  if (leftObject instanceof TemplatePrimitive<?> leftPrimitive && rightObject instanceof TemplatePrimitive<?> rightPrimitive) {
    TemplatePrimitive<?> result = leftPrimitive.relational(type, rightPrimitive, context);
    context.getStatus().expression().addAndGet(2);
    return result;
  }
  return new TemplateRelational(type, leftObject, rightObject);
}

TemplateRelational implements the relational operators <, ≤, ≥, > and <=>. To reduce such an expression, the two operands are reduced first. If both operands are primitive model types, the result of the relation is calculated and returned as a replacement. In the other case, there is a new TemplateRelational instance with the reduced operands.

This feature will be part of the next Freshmarker version 2.2.0 and will gradually be extended to all directives.

Leave a Comment