“We cling nervously to the melody, but we don’t handle it freely, we don’t really make anything new out of it, we merely overload it.”
Johannes Brahms
Operator overloading is a programming technique in which an existing operator is extended or adapted to user-defined data types. This makes it possible to design user-defined classes in such a way that they behave syntactically like built-in data types and facilitates intuitive and readable use of these types using the familiar operators.
Overloaded operators have various advantages for the source code. They can make the code much more intuitive and easier to understand, especially when processing complex data structures. Less type checking and type conversion is required as this is already handled in the overloading code. Similar data types or structures can be handled uniformly, which improves the consistency of the code.
Every coin has two sides and so of course there are some disadvantages. Developers who are not familiar with the concept may find it difficult to immediately understand the overloaded code. If operators are overloaded inconsistently, this can lead to difficulties in predicting behavior, which can lead to errors and unexpected behavior.
In languages such as C++ and Python, the developer can overload operators at language level. With FreshMarker, however, the extension mechanism for new model types should provide for operator overloading. Overloading from the template language is not intended.
The idea for overloading the operators did not fall from the sky, but came about during a refactoring session in the TemplateOperator
class. The class evaluates the numeric operations +
, -
, /
, *
, %
and the string concatenation.
@Override public TemplateObject evaluateToObject(ProcessContext processContext) { TemplateObject leftValue = left.evaluateToObject(processContext); TemplateObject rightValue = right.evaluateToObject(processContext); if (op == TokenType.PLUS) { Optional<TemplateString> leftString = leftValue.asString(); Optional<TemplateString> rightString = rightValue.asString(); if (leftString.isPresent() && rightString.isPresent()) { return leftString.get().concat(rightString.get()); } } TemplateNumber leftNumber = leftValue.evaluate(processContext, TemplateNumber.class); TemplateNumber rightNumber = rightValue.evaluate(processContext, TemplateNumber.class); return switch (op) { case PLUS -> leftNumber.add(rightNumber); case MINUS -> leftNumber.subtract(rightNumber); case TIMES -> leftNumber.multiply(rightNumber); case DIVIDE -> leftNumber.divide(rightNumber); case PERCENT -> leftNumber.modulo(rightNumber); default -> throw new ProcessException("unsupported operation: " + op); }; }
At the beginning of the method, the two operands are evaluated and, depending on the type and the operand, the string concatenation or one of the numeric functions is called.
One idea to structure this code better is to move the actual calculation to the respective model classes TemplateString
and TemplateNumber
.
A default method operation is provided in the TemplateObject
interface for this purpose. It receives the second operand and the operator as parameters and returns the result of the operation as the return value.
default TemplateObject operation(TokenType operator, TemplateObject operand, ProcessContext context) { throw new ProcessException("unsupported operation: " + operator); }
This new method greatly simplifies the evaluateToObject
method.
@Override public TemplateObject evaluateToObject(ProcessContext processContext) { TemplateObject leftValue = left.evaluateToObject(processContext); TemplateObject rightValue = right.evaluateToObject(processContext); return leftValue.operation(op, rightValue, processContext); }
The two operands are evaluated and then the operation method is called on the left operand. For the string concatenation to continue to work, the TemplateString
class must override the operation
method.
@Override public TemplateObject operation(TokenType operator, TemplateObject operand, ProcessContext context) { if (operator != TokenType.PLUS) { super.operation(operator, operand, context); } return concat(operand.evaluate(context, TemplateString.class)); }
The implementation adds the right operand to the left operand as a template string. If an operator other than +
is used, the implementation throws an exception.
The implementation for TemplateNumber
is a few lines longer, because it has to support all operators.
@Override public TemplateObject operation(TokenType operator, TemplateObject operand, ProcessContext context) { TemplateNumber rightNumber = operand.evaluate(context, TemplateNumber.class); return switch (operator) { case PLUS -> add(rightNumber); case MINUS -> subtract(rightNumber); case TIMES -> multiply(rightNumber); case DIVIDE -> divide(rightNumber); case PERCENT -> modulo(rightNumber); default -> super.operation(operator, operand, context); }; }
Both implementations throw an exception if the right operand is not a TemplateString
or TemplateNumber
instance.
This restores the original functionality after refactoring. The new implementation is not only simpler, but also more elegant from an OOP perspective.
But the new implementation has another charming side: the operation method can be implemented by all model types, internal as well as custom one.
public class TemplatePeriod extends TemplatePrimitive<Period> { public TemplatePeriod(Period value) { super(value); } @Override public TemplateObject operation(Token.TokenType operator, TemplateObject operand, ProcessContext context) { TemplatePeriod period = operand.evaluate(context, TemplatePeriod.class); return switch (operator) { case PLUS -> new TemplatePeriod(getValue().plus(period.getValue())); case MINUS -> new TemplatePeriod(getValue().minus(period.getValue())); default -> super.operation(operator, operand, context); }; } }
Here, the TemplatePeriod
class overwrites the operation
method. This means that periods can be added up or differences calculated.
${period1} + ${period2} = ${period1 + period2}
The above template fragment generates the following output for the two periods P3D
and P2D
.
P3D + P2D = P5D
In the next article, the relational operators <
, >
, ≤
and ≥
are overwritten, to make it easier to formulate expressions over non numeric model types like TemplateVersion
.
1 thought on “FreshMarker Operator Overloading”