“They must often change, who would be constant in happiness or wisdom.”
Confuzius
The FreshMarker Switch Directive is an unusual representative of its kind. Since it has been implemented according to the possibilities of its syntax description in the CongoCC example source code, it is capable of quite unusual working methods. Some special cases could be processed faster with an optimized version. Therefore, the Switch Directive is optimized for constants in this article.
The reason for the possibilities of the Switch Directive is hidden in the CongoCC grammar for FreshMarker.
Switch #SwitchInstruction : (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>) <SWITCH> =>|| <BLANK> Expression <CLOSE_TAG> [<WHITESPACE>] (Case)* [ SCAN 2 Default ] DirectiveEnd("switch") ; Case #CaseInstruction : (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>) (<CASE>|<ON>) =>|| <BLANK> Expression (<COMMA> Expression)* <CLOSE_TAG> Block ;
As you can see from this excerpt, a SwitchInstruction
consists of a switch tag with an Expression
and any number of CaseInstructions
. The CaseInstruction
in turn consists of a case or on tag with any number of Expressions
and a subsequent Block
. A grammar does not normally say everything about a programming language. Many parts of the semantics of a language result from how parts of the grammar are used.
A simple example is the CaseInstruction
, apparently a case tag, just like the on tag, can contain any number of Expressions
. Although the syntax offers the possibility, the FreshMarker interpreter does not allow this. An error is output for case tags with more than one expression. Only the on tag may contain several Expressions
.
The Expressions
allowed in the tags can be arbitrarily complicated Expressions
, but the values should be evaluated to the same primitive FreshMarker types. Firstly, because the necessary comparisons in FreshMarker are only defined on primitive types and secondly, of course, so that the expression in the switch tag can match at least one expression in the case tag.
The story with the same type is not so simple in FreshMarker, because the type of the result can only be determined when the Expression
is evaluated. This is because FreshMarker still has no type information stored in the template.
The following example shows the possibilities and problems of the FreshMarker Switch Directive.
<#switch 42> <#case answer> the answer to the ultimate question of life, the universe, and everything. <#case 6 * 7> the ultimate question. <#case question> the ultimate question. <#case 'fortytwo'> the answer to the ultimate question of life, the universe, and everything. </#switch>
Unlike usual, in this example the switch tag does not contain a variable but a constant. This is possible, but also means that there should be a case tag whose Expression
leads to this constant. In this example, three of the case tags can fulfill this condition. If the variables question
or answer
contain the value 42, then they can fulfill the condition, otherwise the second case tag fulfills the condition, because the Expression
6*7 always results in 42. The last case tag is never used because the Expression
always results in 'fortytwo'
and, of course, the second case tag always wins.
Why does the second case tag win? The ConditionalFragment
instances representing the CaseInstructions
are processed in the order in which they occur because each Expression
can potentially be successful.
This is quite a weird example, normally the Switch Directive is used differently.
<#switch value> <#case 1>Eins/One <#case 2>Zwei/Two <#case 3>Drei/Three </#switch>
A variable value
is specified in the switch tag and all case tags contain an integer constant. Depending on the value of the variable, one of the ConditionalFragment
instances can then be evaluated. As all three constants differ from each other, it does not matter which of them is checked first.
This type of switch directive allows us to switch from the previous implementation as List<ConditionalFragment>
to another familiar data structure. In this case Map<TemplatePrimitive<?>, Fragment>
is the better choice. For our example, the primitive keys would be TemplateNumber
instances of 1, 2 and 3 and the values, ConstantFragment
instances for the values Eins/One
, Zwei/Two
and Drei/Three
.
public class MapSwitchFragment extends AbstractConditionalFragment implements SwitchFragment { private final TemplateObject switchExpression; private final Map<TemplatePrimitive<?>, Fragment> fragmentMap; // ... public void process(ProcessContext context) { TemplatePrimitive<?> switchValue = evaluatePrimitive(this.switchExpression, context, node); fragmentMap.getOrDefault(switchValue, endFragment).process(context); } }
The implementation of the new MapSwitchFragment
ist trivial, it contains the switchExpression
containing the Expression
from the switch tag and the fragmentMap
described above. The process
method is also implemented simply, the switchValue
is evaluated and then the appropriate entry from the fragmentMap
is used. If no suitable entry is found, the default case is available with endFragment
.
Die Einfachheit der MapSwitchFragment erkauft man sich durch eine größere Komplexität bei der Auswertung der Switch Direktive.
@Override public SwitchFragment visit(SwitchInstruction ftl, SwitchFragment input) { Node expression = ftl.get(3); TemplateObject switchExpression = expression.accept(interpolationBuilder, null); Map<NodeType, List<CaseInstruction>> parts = ftl.childrenOfType(CaseInstruction.class).stream().collect(groupingBy(p -> p.get(1).getType())); List<CaseInstruction> caseParts = parts.getOrDefault(TokenType.CASE, List.of()); List<CaseInstruction> switchOnParts = parts.getOrDefault(TokenType.ON, List.of()); if (!caseParts.isEmpty() && !switchOnParts.isEmpty()) { throw new ParsingException("switch directive contains on and case", ftl); } if (!caseParts.isEmpty()) { handle(caseParts, collector, ftl, featureSet.isEnabled(ALLOW_ONLY_CONSTANT_CASES) && featureSet.isEnabled(ALLOW_ONLY_EQUAL_TYPE_CASES)); } if (!switchOnParts.isEmpty()) { handle(switchOnParts, collector, ftl, featureSet.isEnabled(ALLOW_ONLY_CONSTANT_ONS) && featureSet.isEnabled(ALLOW_ONLY_EQUAL_TYPE_ONS)); } DefaultInstruction defaultPart = ftl.firstChildOfType(DefaultInstruction.class); if (defaultPart != null) { defaultPart.accept(this, null); } if (featureSet.isEnabled(SwitchDirectiveFeature.OPTIMIZE_CONSTANT_SWITCH) && collector.isAllPrimitive) { Map<TemplatePrimitive<?>, Fragment> switchMap = collector.fragments.stream() .collect(toMap(c -> (TemplatePrimitive<?>) c.conditional(), ConditionalFragment::content, (a, b) -> a)); if (switchMap.size() < collector.fragments.size() && featureSet.isEnabled(SwitchDirectiveFeature.ERROR_ON_DUPLICATE_CASE_EXPRESSION)) { throw new ParsingException("switch with duplicate conditionals", ftl); } return new MapSwitchFragment(switchExpression, expression, switchMap, collector.fragment); } return new ListSwitchFragment(switchExpression, expression, collector.fragments, collector.fragment); }
The previous SwitchFragment
implementation has been moved to the ListSwitchFragment
class. ListSwitchFragment
and MapSwitchFragment
both implement the new SwitchFragment
interface. Previously, a SwitchFragment
was created directly; now the ConditionalFragments
are collected first and a decision is made at the end of the method as to which implementation should be used.
When collecting, the system checks whether the expression of the ConditionalFragment
instances are all constants. If this is the case, a MapSwitchFragment
can be created, otherwise a ListSwitchFragment
.
Two new FreshMarker features round off the use of the MapSwitchFragment
. The new implementation can be activated with OPTIMIZE_CONSTANT_SWITCH
. If it proves successful, this feature will be removed in one of the following versions. With ERROR_ON_DUPLICATE_CASE_EXPRESSION
an error message is thrown if a constant is used more than once. This is checked via the cardinality of the created Map
and the collecting List
. If the Map
contains fewer entries, then a second occurrence of a constant was rejected by the expression (a, b) -> a
.
The new MapSwitchFragment
should not only be able to be evaluated, but also reduced by the Partial Template Reduction mechanism.
@Override public Fragment reduce(ReduceContext context) { try { TemplatePrimitive<?> switchValue = evaluatePrimitive(switchExpression, context, node); return fragmentMap.getOrDefault(switchValue, endFragment).reduce(context); } catch (WrongTypeException e) { throw new ReduceException(e.getMessage(), node, e); } catch (ProcessException ignored) { return new MapSwitchFragment(switchExpression, node, reduceMap(context), endFragment.reduce(context)); } }
The Fragment
can be replaced if the switchVariable
can be evaluated and no exception is thrown when the Fragment
from the Map
is reduced. In this case, the SwitchFragment
is replaced by the reduced Fragment
from a case tag or the default tag. If an error occurs, all Fragment
instances from the Map
are reduced and a new MapSwitchFragment
is created with the newly created Map
.
The goal of optimization was also achieved with the new implementation. The more case tags there are in the Switch Directive, the greater the speed advantage of the new implementation. The position and visit frequency of the case tags must also be considered. Simple measurements have so far shown a doubling of the speed.
This feature will be part of the next version 1.11.0 of FreshMarker. Stay tuned!