“Pain is inevitable. Suffering is optional.”
Haruki Murakami
Agile software development is characterized, among other things, by the fact that features are developed on demand. This is one of the reasons why some features are not available in FreshMarker or are only added very late.
Examples of late additions are the Include- and Import-Directives and the support of the StringBuilder
and StringBuffer
data types. For the former, the use case was not planned at the start of development and for the latter, support was simply forgotten. Another forgotten data type is the Optional
class, which is now finally to be supported.
The Optional
class is a special data type. It makes dealing with the presence or absence of a value safer and more explicit without directly representing a concrete value itself. The Optional
data type as a monad makes it possible to work safely and elegantly with optional values by providing an expressive and comprehensible way of handling errors and processing possibly absent values without explicit null checks.
Optional
values are not supported in the current FreshMarker version. If the template engine detects an optional
value in the data model, processing ends with the corresponding exception.
The implementation of the Optional
class should not bring any unnecessary ballast with it. The Optional
is only a construct to represent the presence or absence of a value. In FreshMarker, the presence or absence is realized via the Existence and the Default Operator. It is therefore not necessary to define optionals as a separate model type; instead, the values they contain are transferred to the model.
The following example shows the template for a model variable optional
on the left, which contains the value Optional.of(42)
.
${optional} ${optional??} ${optional!23}
42 yes 42
The first interpolation returns the value 42
, because FreshMarker does not work with the Optional
instance, but with the contained Integer
instance. The Existence Operator returns true
, which is converted to yes
in the output. In this case, the Default Operator also returns 42
because the value exists.
${optional??} ${optional!23}
no 23
If the model variable optional
contains the value Optional.empty()
, the output changes. The first interpolation was omitted because it throws an exception for a missing value. The Existence Operator returns false
, which is converted to the output no
. As the value is not set, the Default Operator returns 23
in this case.
To work within the template engine with a value that does not exist in the final version in the model, there is a suitable place. FreshMarker supports lazy values, i.e. the actual value is only determined and used when the template engine accesses such a value. To do this, the model variable is checked to see if it is of the TemplateObjectSupplier
type before it is wrapped..
Object current = o instanceof TemplateObjectSupplier<?> templateObject ? templateObject.get() : o;
Here the get
method is called on the Supplier
and the actual value is determined. What could be more obvious than to check the Optional
type at this point.
Object current; if (o instanceof Optional<?> optional) { if (optional.isEmpty()) { return TemplateNull.NULL; } current = optional.get(); } else if (o instanceof TemplateObjectSupplier<?> templateObjectSupplier) { current = templateObjectSupplier.get(); } else { current = o; }
The implementation has become a little longer, the evaluation of the Optional
has been added. If the Optional
is empty, the value TemplateNull.NULL
is used, otherwise the value from the Optional
.
The implementation of the feature is now complete and the optional feature will be supported in the next version of the template engine.