In the first article on the implementation of the Partial Template Reduction feature, a few basic functions were created. This part deals with the implementation for the If-, Switch- and List- Directives.
If- and Switch-Directives both have a very similar structure. The IfFragment
and the SwitchFragment
each contain a list fragments
of ConditionalFragment
and an optional Fragment
named endFragment
. The list corresponds to the Case blocks of the Switch-Directive and the If and ElseIf blocks in the If-Directive. The optional Fragment
corresponds to the Default and the Else block. When evaluating the conditional directives, their internal list of ConditionalFragment
instances is run through until one of them match the condition.
If a matching ConditionalFragment
is found, the overall processing is continued with its internal Fragment
. If no suitable ConditionalFragment
is found, the processing may continue with the endFragment
.
A positive match of a ConditionalFragment
means that true
is returned when the expression is evaluated. This means for the ConditionalFragment
instances in the IfFragment
, the expressions in the filterByConditional
method are compared with TemplateBoolean.TRUE
. If the comparison is successful, the corresponding fragment is reduced, otherwise the endFragment
is reduced.
public class IfFragment implements Fragment { // .. @Override public Fragment reduce(ReduceContext context) { try { for (ConditionalFragment fragment : fragments) { if (filterByConditional(context, fragment)) { return fragment.reduce(context); } } return endFragment.reduce(context); } catch (RuntimeException e) { log.info("cannot reduce: {}", e.getMessage(), e); } return new IfFragment(fragments.stream().map(f -> f.reduce(context)).toList(), endFragment.reduce(context)); } }
For the ConditionalFragment
instances in the SwitchFragment
, the expression in the Switch-Directive is evaluated first and then compared with the expressions of the individual Cases. In fact, complex expressions and not just constants can be used in a Case in FreshMarker. In the future, there may be an optimization for constant cases.
public class SwitchFragment implements Fragment { // ... @Override public Fragment reduce(ReduceContext context) { try { TemplatePrimitive<?> switchValue = evaluatePrimitive(this.switchExpression, context, node); for (ConditionalFragment fragment : fragments) { if (switchValue.equals(evaluatePrimitive(fragment.getConditional(), context, fragment.getNode()))) { return fragment.reduce(context); } } return defaultFragment.reduce(context); } catch (RuntimeException e) { log.info("cannot reduce: {}", e.getMessage(), e); } SwitchFragment switchFragment = new SwitchFragment(switchExpression, node); switchFragment.fragments.addAll(fragments.stream().map(f -> f.reduce(context)).toList()); switchFragment.defaultFragment = defaultFragment.reduce(context); return switchFragment; } }
The same applies to the SwitchFragment
. If the comparison is successful, the corresponding Fragment
is reduced, otherwise the endFragment
is reduced. Both Fragment
implementations have the getSize
implementation in common. The size is defined as the sum of the sizes of the sub-fragments plus 1.
@Override public int getSize() { return fragments.stream().mapToInt(Fragment::getSize).sum() + endFragment.getSize() + 1; }
The ConditionalFragment
instances contain an expression conditional
and a Fragment
named content
.
public class ConditionalFragment implements Fragment { private final TemplateObject conditional; private final Fragment content; // ... @Override public ConditionalFragment reduce(ReduceContext context) { try { BlockFragment reduce = content.reduce(context); if (reduce == content) { return this; } context.getStatus().changed().incrementAndGet(); return new ConditionalFragment(conditional, reduce, node); } catch (RuntimeException e) { return this; } } @Override public int getSize() { return content.getSize() + 1; } }
The reduce
method reduces the content
and returns a new ConditionalFragment
if the content
has actually changed. In the other case, the current ConditionalFragment
is simply returned.
At this point there is another possibility to reduce the templates. The expressions in the ConditionalFragments
could actually also be reduced if the expressions could be simplified. However, this is reserved for a later version of Partial Template Reduction.
One last interesting Directive to support is the List-Directive. Its two implementations for Hashes and Sequences can be found in the corresponding Fragment
classes HashListFragment
and SequenceListFragment
. They contain a Fragment
for the block
that is to be run through multiple times.
@Override public SequenceListFragment reduce(ReduceContext context) { return optimize(block, block.reduce(context), r -> new SequenceListFragment(list, identifier, looperIdentifier, r, ftl)) } @Override public HashListFragment reduce(ReduceContext context) { return optimize(block, block.reduce(context), r -> new HashListFragment(list, keyIdentifier, valueIdentifier, looperIdentifier, r, ftl, comparator)); }
To reduce, the internal block
is reduced for both implementations and its result is evaluated in the optimize
method. The optimize
method looks at three cases. If the result of the reduction is ConstantFragment.EMPTY
, then the list has no body and the entire list is replaced by ConstantFragment.EMPTY
. If the reduction returns the identical object, then the list is unchanged and returned as usual. Otherwise, a new Fragment
is created.
This implementation therefore already specifies the restriction of the reduce
method for the List-Directive. As the evaluation is only carried out with the variables from the partial model, there are no loop variables that can be used for reduction. Only the invariants in the list can be reduced. Variables within the list should also not hide model variables, otherwise the wrong values will be reduced. However, this shortcomings will be corrected in the next version of the feature.
Apart from one small crucial detail, all the important details for the Partial Template Reduction feature have now been implemented. There were a few more fragments to adapt, but their changes are boring. The following example shows the possibilities of the new feature so far.
<#switch flag> <#case 1>${company} <#case 2>${name!'Jens'} <#case 3><#if name??>${name}<#else>Anonymous</#if> <#default>default </#switch>
For the reduction with template.reduce(Map.of("flag", 2)
or template.reduce(Map.of("flag", 3)
, we expect the result to be a Template
that generates Regina
as the output when processing with the value Regina
for name
.
With the previous changes, however, we get the output Jens
for 2
and the output Anonymous
for 3
. Here, the Default– and Exist-Operators do not yet work correctly with the reduce
method. Both operators do not find the value for name
and therefore evaluate the expression incorrectly. This results in an undesired reduction. To prevent this reduction, the evaluateToObject
methods must be informed that it is a reduce
call and not a process
call.
@Override public TemplateObject evaluateToObject(ProcessContext context) { TemplateObject templateObject = expression.evaluateToObject(context); return context.reductionCheck(templateObject) ? TemplateBoolean.TRUE : TemplateBoolean.FALSE; }
The ProcessContext
class has an implementation of reductionCheck
that returns true
if the expression is not evaluated to a TemplateNull
value. This method is overwritten in the ReduceContext
so that a ReduceException
is thrown for a TemplateNull
value. This cancels the reduction of the corresponding fragment.
In the example shown above, 12 of the original 22 Fragment
instances are removed. The whole SwitchFragment
is replaced by the ConditinalFragment
for case 3.
The Partial Template Reduction feature can be used with the latest version of FreshMarker on Maven Central.
1 thought on “Partial Template Reduktion (2)”