Partial Reduction of Range Lists

One of the most useful optimizations in FreshMarker is partial reduction: templates can be partially evaluated in advance if part of the model is already known. Until now, however, partial reduction had a blind spot—List directives with ranges (1..4, 4..1, 1..) were ignored during list unrolling and passed through as simple, non-reduced fragments. This changes with version 2.7.0.

The problem: Ranges were ignored and forgotten

Until now, the SequenceListFragment class did not distinguish between a normal sequence and a range object during reduction. In getList(ReduceContext), it simply checked for TemplateListSequence—if that was not the case, it simply returned List.of(), and the reduction was terminated with simpleReduce. What was intended to be a temporary workaround during development became a permanent solution.

// before
private List<Object> getList(ReduceContext context) {
  TemplateObject templateObject = list.evaluateToObject(context);
  if (templateObject instanceof TemplateListSequence(List<Object> sequence)) {
    return filterSequence(context, sequence);
  }
  return List.of(); // Range: silent misfire
}

The solution: a custom ReductionStrategy

New RangeListStrategy class

Until now, the differences in the partial reduction of hashes and sequences in the List directive have been handled using the Strategy pattern. The HashListStrategy class handles the reduction of List directives with hashes, and the SequenceListStrategy class handles the reduction of List directives with sequences. Both inherit from the ReductionStrategy class.

At the heart of this change is the new strategy specifically designed for ranges:

public class RangeListStrategy implements ReductionStrategy {
  private final String identifier;
  private final List<Object> sequence;

  public RangeListStrategy(String identifier, TemplateRange list) {
    this.identifier = identifier;
    this.sequence = list.sequence();
  }

  @Override
  public void handle(List<Fragment> fragments, int index) {
    if (fragments.stream().allMatch(this::needsVariableContext)) {
      fragments.addFirst(new VarVariableFragment(identifier, TemplateNumber.of((Integer) sequence.get(index)), null));
    }
  }
}

A unique feature of the RangeListStrategy is that you don’t need to insert a TemplateDynamicKey to retrieve the value from sequence. With the range, the corresponding position in sequence always contains an integer.

The method SequenceListFragment.getStrategy selects the appropriate strategy:

private ReductionStrategy getStrategy(TemplateObject templateObject, int unfoldLimit) {
  if (templateObject instanceof TemplateRange range) {
    return new RangeListStrategy(identifier, range, unfoldLimit);
  }
  return new SequenceListStrategy(identifier, list, unfoldLimit);
}

The getList method has been updated: It now receives the TemplateObject that has already been evaluated (instead of evaluating it a second time internally) and now also recognizes TemplateRange:

private List<Object> getList(ReduceContext context, TemplateObject templateObject) {
  return switch (templateObject) {
    case TemplateListSequence(List<Object> sequence) -> filterSequence(context, sequence);
    case TemplateRange range -> filterSequence(context, range.sequence(), range.isRightUnlimited();
    case null, default -> List.of();
  };
}

The third parameter of the filterSequence method for ranges checks whether a limit has also been specified for right-unlimited ranges in the List directive. Without a limit, quasi-infinite lists (the length is technically limited to 2,147,483,647 entries) are not permitted in the List directive.

What this means in practice

Given this template—where company is still unknown at the time of reduction, but the range is known:

<#list 1..4 as s>${s} ${company} </#list>

Before: The template remained unchanged after reduce() (7 fragments).

After: The loop is expanded. The reduced template contains 29 fragments—the four iterations have been materialized, each with the specific number, and only company remains as an open variable:

Fazit

This change is a good example of the extension principle in FreshMarker: rather than complicating SequenceListFragment, a dedicated strategy class was introduced to encapsulate the specific semantics of ranges.

Leave a Comment