List Directive Unrolled

“The present is the past rolled up for action, and the past is the present unrolled for understanding.”

Ariel Durant

The Partial Template Reduction feature allows FreshMarker templates to be simplified. Part of the model is used to evaluate expressions in directives and interpolations and generate a new template. A template reduced in this way contains fewer dynamic and more static parts and can therefore be evaluated more quickly. Until now, list directives were excluded from the simplification options. This can now change under certain circumstances.

To get a better understanding of the mechanism behind Partial Template Reduction, it is illustrated here using a small example.

${''?right_pad(32, '•●⬤●')}
Name:    ${firstname} ${lastname}
<#if email??>
E-Mail:  ${email?lower_case}
<#else>
E-Mail:  ●●●
</#if>
Company: ${company}
${''?right_pad(32, '•●⬤●')}
Map.of(
  "email", "Wile.E.Coyote@acme.com",
  "company", "ACME"
);
•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●
Name:    ${firstname} ${lastname}
E-Mail:  wile.e.coyote@acme.com
Company: ACME
•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●

The template on the left contains various interpolations and an If Directive. To evaluate this template, the variables firstname, lastname, email and company must be contained in the data model. However, a subset of these variables is sufficient for a reduction of the template. A subset consisting of email and company is shown in the center. If the template is reduced with this subset, a new template is obtained that corresponds to the template shown on the right. You can see that all interpolations, except for those with the variables firstname and lastname, have been calculated and are now replaced by constants. The If Directive could also be replaced by its first block because the expression email?? could be evaluated.

The new template is much more compact and contains almost only static content. If this template is evaluated, only two interpolations need to be calculated.

The situation is far more complicated when a List Directive is involved. As the values of the loop variables change in each run, no simplifications can be made.

${''?right_pad(32, '•●⬤●')}
<#list employees as e with l>
Name:   ${e.firstname} ${e.lastname}
<#if e.email??>
E-Mail: ${e.email?lower_case}
<#else>
E-Mail: ●●●
</#if>
Company: ${company}
<#if l?has_next>
${''?right_pad(32, '•●')}
</#if>
</#list>
${''?right_pad(32, '•●⬤●')}
String acme = "@acme.com";

List<Employee> employees = List.of(
  new Employee("Wile.E.Coyote" + acme),
  new Employee("Elmar.J.Fudd + acme)
);

Map.of(
  "employees", employees,
  "company", "ACME"
);
•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●
<#list employees as e>
Name:    ${e.firstname} ${e.lastname}
<#if e.email??>
E-Mail:  ${e.email?lower_case}
<#else>
E-Mail:  ●●●
</#if>
Company: ACME
<#if l?has_next>
${''?right_pad(32, '•●')}
</#if>
</#list>
•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●

So far, Partial Template Reduction has not simplified anything in the List Directive, because all expressions depend on the loop variables e and l. Although both e-mail addresses are known, they cannot be used for reduction because they populate the variables with different values in both loop passes.

One solution to the problem is loop unrolling. This is an optimization mechanism for computer programs in which a loop is replaced by several copies of its loop body. The respective copies are populated with the appropriate values of the respective iteration. This simplifies the execution because no loop condition has to be checked, no jumps back and the respective loop bodies can be optimized.

For the Partial Template Reduction mechanism of FreshMarker, this means that if the prerequisites for loop unrolling are met, then the reduce method of the ListFragment classes should reduce with copies of the loop body for each value in the list and insert it into a new Fragment. The new Fragment is then no longer a loop, but a sequence of similar BlockFragment instances.

The implementation is not quite as simple as described here because there is a small problem with the loop variables. Without an outer loop, the expression ${e.firstname} in the reduced template is empty. However, a simple solution to this problem already exists in FreshMarker in the form of the Var Directive. At the beginning of each copied BlockFragment, we insert a VarVariableFragment. This defines a variable with the name of the loop variable to access the correct element of the list.

The looper variable, which allows access to the metadata of the loop, does not need to be inserted because it is always completely reduced.

For a List Directive that accesses a hash, two variables must be defined, one for the key and one for the value of the hash entries.

So that unnecessary variables are not continuously defined when the entire loop body has been reduced, the reduction mechanism first checks whether the reduced loop body requires a variable. The implementation is kept trivial because reducing is a rather rare function. If the loop body only contains ConstantFragment and BlockFragment instances, variables are unnecessary.

The new Partial Template Reduction mechanism can better reduce the above example, as outlined below.

${''?right_pad(32, '•●⬤●')}
<#list employees as e with l>
Name:   ${e.firstname} ${e.lastname}
<#if e.email??>
E-Mail: ${e.email?lower_case}
<#else>
E-Mail: ●●●
</#if>
Company: ${company}
<#if l?has_next>
${''?right_pad(32, '•●')}
</#if>
</#list>
${''?right_pad(32, '•●⬤●')}
String acme = "@acme.com";

List<Employee> employees = List.of(
  new Employee("Wile.E.Coyote" + acme),
  new Employee("Elmar.J.Fudd + acme)
);

Map.of(
  "employees", employees,
  "company", "ACME"
);
•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●
<#var e=employees[0]>
Name:   ${e.firstname} ${e.lastname}
E-Mail: wile.e.coyote@acme.com
Company: ACME
•●•●•●•●•●•●•●•●•●•●•●•●•●•●•●•●
<#var e=employees[1]>
Name:   ${e.email?lower_case}
E-Mail: elmar.j.fudd@acme.com
Company: ACME
•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●•●⬤●

The loop resulted in two blocks in which the email addresses were successfully replaced. The separator line could also be replaced because the If Directive could be reduced in both cases. In the first case to the separator line and in the second case to an empty Fragment. The Var Directive is only used to indicate the variable definition within the template.

The next article on List Directive Unrolling will present some details of the implementation. List Directive Unrolling will be part of the next version of FreshMarker. Stay tuned.

Leave a Comment