Adhuc tua messis in herba est.
Ovid
To implement a new built-in for sequences, lambdas were added as a new language component of the FreshMarker Expression Language. The lambda expression made it much easier to map the semantics of the new count built-in in an easily readable syntax. Now that lambda expressions are available, what else can they be used for and what still needs to be implemented?
Lambda was used in a new built-in for sequences, so the search for other possibilities initially led to the existing built-ins for sequences. Interestingly, first was the first existing built-in to be extended with lambdas. Until now, this built-in returned the first element of a sequence.
${[1, 2, 3, 4, 5, 6]?first}!
In this case, the interpolation produces the output 1! because 1 is the first element of the sequence. With the use of lambdas, there is now another use for this built-in.
${[1, 2, 3, 4, 5, 6]?first(n -> n > 3}!
In this case, the interpolation produces the output 4!, because 4 is the first element of the sequence that satisfies the expression n > 3 in the lambda. The same addition can be made to the built-in last, which now also returns the last element that satisfies the expression in the passed lambda.
${[1, 2, 3, 4, 5, 6]?last(n → n < 4}!
In this case, the interpolation produces the output 3! because this is the last element that satisfies the expression in the lambda.
The implementation of the first built-in changes as follows.
private static TemplateObject first(TemplateListSequence value, List<TemplateObject> parameters, ProcessContext context) {
if (parameters.isEmpty()) {
return value.get(context, 0);
}
if (!(parameters.getFirst() instanceof TemplateLambda lambda)) {
throw new ProcessException("only lambdas allowed here");
}
Environment old = context.getEnvironment();
VariableEnvironment variableEnvironment = new VariableEnvironment(old);
context.setEnvironment(variableEnvironment);
try {
List<Object> sequence = value.sequence();
for (Object object : sequence) {
TemplateObject templateObject = context.mapObject(object);
variableEnvironment.createVariable(lambda.getVariable(), templateObject);
if (lambda.evaluate(context, TemplateBoolean.class) == TemplateBoolean.TRUE) {
return templateObject;
}
}
return TemplateNull.NULL;
} finally {
context.setEnvironment(old);
}
}
Nothing changes when used without parameters. The first element of the list is selected and returned. If the first parameter is not a lambda expression, a ProcessException is thrown. A VariableEnvironment is set for the lambda variables in front of the current environment, and then the sequence is traversed with the updated context. If the lambda expression returns TRUE, the loop is terminated, the context is reset, and the current element is returned. If no matching element is found, NULL is returned.
The last built-in works similarly, except that the list is traversed from the end to the beginning. As can be seen in the example for the that built-in, the Unicode character → can also be used for the lambda operator ->. Since other operators also have a Unicode counterpart, this was consistent and the change in the grammar is also very simple.
<LAMBDA : "->" | "→">
The CongoCC parser generator works cleanly with Unicode characters, so it is sufficient to add the LAMBDA token here as well.
Several additional plugins are planned for sequences to generate new ones from existing ones.
- A
filterbuilt-in that creates a new list by not including all elements of the original list. - A
mapbuilt-in that creates a new list with the same number of elements, but with modified elements. - A
frombuilt-in that creates a new list from the suffix of another list. The suffix begins as soon as one of the elements matches the expression. - An
untilbuilt-in that creates a new list from the prefix of another list. The prefix ends as soon as one of the elements no longer matches the expression.
A small bug had to be removed from the implementation for the next release. Lambdas are now allowed everywhere due to the grammar, but they are only desired in a few built-ins. The implementations shown so far use the VariableEnvironment to access the lambda parameters. Where no lambda is expected, the lambda searches for a suitable variable and may find an incorrect value or no value at all. Therefore, we need to remedy this situation.
To avoid checking in many places whether a lambda is allowed, we reverse the responsibility and let the lambda check whether it can be called. A simple solution here is to check the environment.
@Override
public TemplateObject evaluateToObject(ProcessContext context) {
if (context.getEnvironment().isLambda()) {
throw new ProcessException("no lambdas allowed here");
}
return lambda.evaluateToObject(context);
}
The environments receive the isLambda method, which returns false by default. Only during lambda processing is a modified VariableEnvironment used that returns true during this check.
This now ensures that lambdas can only be used where their behavior is handled correctly. Now we just have to wait for the next release.