More Operator Overloading

“Knowing is not enough; we must apply. Willing is not enough; we must do.”

Johann Wolfgang von Goethe

In the previous article on operator overloading, the implementation for the numerical addition and multiplication operators was presented. In this article, some other operators will be provided with this feature and some details will be discussed that should be taken into account when operator overloading.

The FreshMarker grammar provides a whole range of operators. In addition to the already discussed operators, there are also the relational operators, the equality operators, the logical operation operators and the prefix operators.

Before we implement operator overloading for an operator, we should think about the semantics of the operations that we want to extend to other types. Not everything that is possible makes sense.

Let’s go back to the overloaded addition operators. The overloaded + operator should not contradict the existing rules of addition for the user’s understanding. To put it somewhat casually, a quantity of the type in the result of the addition should behave like the sum of a quantity of the operands. For numerical values, this means the sum of the input values. In the case of string concatenation, the String assembled from the operands.

To put it less casually, we want + operators that follow the associative and commutative law of addition and that have a neutral and an inverse element.

Both + operators presented so far correspond to the associative law (a+b)+c=a+(b+c). With three numbers or String instances, it does not matter in which order the + operator is applied. The result is always identical.

The cummutative law a+b=b+a is not supported by the string concatenation. The problem is that not only a quantity must be taken into account, but the result also reflects the sequence of the operands. In contrast, other types such as TemplateNumber, TemplateMoney or TemplatePeriod can comply with the communative law.

Contrary to the addition in mathematics, the overloaded + operators can also have operands of different types. For example, an Integer can be added to a Date to determine the next day.

This implementation does not violate the associative law in FreshMarker, because the expression yesterday + 1 + 1 produces the result tomorrow in both evaluation sequences. However, it technically violates the commutative law because the operation 1 + yesterday is unknown in the TemplateNumber type.

Operators with different operand types should be implemented with care and explicitly documented so that the user is not confused by unexpected results.

Neutral and inverse elements are not important for the implementation of operator overloading, but their consideration can simplify the calculations. With string concatenation, no new String instance needs to be created if one of the two operands is the empty String. When adding an empty Period, the original Period can be returned.

Returning to the other operators, it should be considered which operators should also be overloaded. The equality operators = and \neq do not require operator overloading because they are defined for all (primitive) types. And to be honest, it is difficult to find further application scenarios for logical operators.

This leaves the prefix operators + and -, as well as the relational operators <, \leq, \geq and >.

In this article, we will look at relational operators. The semantics of the operators when overloaded should also be considered here. All (primitive) FreshMarker types could overload the operators, but this only makes sense if the types have a natural order. Instead of explaining reflexivity, antisymmetry, transitivity and totality of a total order here, we leave it at the implementation of the java.util.Comparable interface. Relational operators should only be overloaded for classes that implement the Comparable interface.

The relational operators have so far only been implemented for numerical types in the TemplateRelational class.

@Override
public TemplateBoolean evaluateToObject(ProcessContext context) {
  TemplateNumber leftValue = left.evaluate(context, TemplateNumber.class);
  TemplateNumber rightValue = right.evaluate(context, TemplateNumber.class);
  return switch (type) {
    case LT -> TemplateBoolean.from(leftValue.compare(rightValue).sign().asInt() < 0);
    case GT -> TemplateBoolean.from(leftValue.compare(rightValue).sign().asInt() > 0);
    case LTE -> TemplateBoolean.from(leftValue.compare(rightValue).sign().asInt() <= 0);
    case GTE -> TemplateBoolean.from(leftValue.compare(rightValue).sign().asInt() >= 0);
    default -> throw new IllegalArgumentException("unsupported relation: " + type);
  };
}

First, both operands are evaluated to TemplateNumber instances and then, depending on the operator, the result of the compare method is evaluated. In order to generalize the processing of the relational expressions, the evaluation is moved to the TemplateObject#relation method, which is called on the left operand.

@Override
public TemplateBoolean evaluateToObject(ProcessContext context) {
    TemplateObject leftValue = left.evaluateToObject(context);
    TemplateObject rightValue = right.evaluateToObject(context);
  return TemplateBoolean.from(leftValue.relation(type, rightValue, context));
}

To ensure that the relational operators continue to work on TemplateNumber instances, this class must override the relation method.

@Override
public boolean relation(TokenType operator, TemplateObject operand, ProcessContext context) {
  TemplateNumber rightValue = operand.evaluate(context, TemplateNumber.class);
  return switch (operator) {
    case LT -> compare(rightValue).sign().asInt() < 0;
    case GT -> compare(rightValue).sign().asInt() > 0;
    case LTE -> compare(rightValue).sign().asInt() <= 0;
    case GTE -> compare(rightValue).sign().asInt() >= 0;
    default -> super.relation(operator, operand, context);
  };
}

First, the (right) operator is evaluated to a TemplateNumber instance and then the result of the compare method is evaluated as before. This completes the refactoring for operator overloading for relational operators.

Another, very similar implementation shows the application of relational operators to the TemplateVersion class. This class supports the evaluation of semantic version numbers with the Built-Ins major, minor, patch, is_before, is_equal, is_after. As you can see from the existing Built-Ins, this class has a natural order.

@Override
public boolean relation(Token.TokenType operator, TemplateObject operand, ProcessContext context) {
  TemplateVersion rightValue = operand.evaluate(context, TemplateVersion.class);
  return switch (operator) {
    case LT -> getValue().compareTo(rightValue.getValue()) < 0;
    case GT -> getValue().compareTo(rightValue.getValue()) > 0;
    case LTE -> getValue().compareTo(rightValue.getValue()) <= 0;
    case GTE -> getValue().compareTo(rightValue.getValue()) >= 0;
    default -> super.relation(operator, operand, context);
  };
}

The right operand is converted into the correct type in order to then evaluate the results of the compare method. With this implementation, relational expressions such as the following can now be used in your own templates.

<#if .version ≥ '1.6.3'?version>With Operation Overloading</#if>

If you would like to try out operator overloading, you can find the current version FreshMarker 1.6.3 as usual on Maven Central.

Leave a Comment