The General Concatenation Operator

“Design in nature is but a concatenation of accidents, culled by natural selection until the result is so beautiful or effective as to seem a miracle of purpose.”

Michael Pollan

The FreshMarker Template Language provides two operators for concatenating string values: the string concatenation operators + and ~. Both operators produce a string from their two operands. The difference between the two operators lies in the way the strings are joined together. The unique feature of the ~ operator leads us to the new general concatenation operator.

The + operator is the standard for string concatenation, which FreshMarker adopted from its predecessor, FreeMarker. With this concatenation, the two strings are simply joined together. The result of concatenating 'supercalifragilistic' + 'expialidocious' is 'supercalifragilisticexpialidocious'.

The ~ operator concatenates its two operands by connecting them with a space. The result of the concatenation 'Marry' ~ 'Poppins' is 'Marry Poppins'. Here it is obvious that the semantics of an operation with ~ can always return only a string. In theory, the ~ operator could be overridden by a model class with different semantics, but as we will see later, this is rarely successful.

The operators in FreshMarker are defined on the respective model classes to implement operator overloading. The mechanism for operator overloading requires that the operations are fundamentally based on the implementation of the left operand.

To illustrate the advantages and disadvantages of this approach, we will explain it using the addition of a date and a number. The interpolation ${date + 11} returns the date 11 days from now for a LocalDate variable date. For August 13, 2026, the result is therefore August 24, 2026.

This is implemented in the operation method of the TemplateLocalDate class.

@Override
public TemplateObject operation(Operator operator, TemplateObject operand, ProcessContext context) {
  TemplateObject duration = operand.evaluateToObject(context);
  return switch (operator) {
    case PLUS -> new TemplateLocalDate(getValue().plus(getValue(duration)));
    case MINUS -> new TemplateLocalDate(getValue().minus(getValue(duration)));
    default -> super.operation(operator, operand, context);
  };
}

private static TemporalAmount getValue(TemplateObject object) {
  if (object instanceof TemplatePeriod period) {
    return period.getValue();
  }
  if (object instanceof TemplateNumber number && !number.getType().isFloatingPoint()) {
    return Period.of(0, 0, number.getValue().intValue());
  }
  throw new ProcessException("wrong type: " + object.getModelType());
}

For the + operator, a new TemplateLocalDate object is created, to which the value of the second operand is added using the plus method. The second operand is determined via the getValue method. Only the Number and Period types are supported here. The advantage of this implementation is the relatively simple and efficient way in which operations on the model types can be performed, and the fact that it is also available for custom model classes.

The disadvantage of this mechanism quickly becomes clear to a mathematician: it does not support the commutative law. The interpolation ${date + 11} cannot be evaluated because the implementation relies on the first operand. The interploation ${11 + date} would mean that the TemplateNumber class must be able to work with LocalDate operands. For every relevant combination of operands, implementations would therefore need to be added to various classes. No suitable and efficient mechanism to avoid this is currently available in FreshMarker.

There has been an exception since FreshMarker 2.4.0 for string operands. In this case, string concatenation is always used if one of the two operands is a string. A shortcut is used for this in the TemplateOperation class. If one of the two operands in + and ~ operations is a string, then the left operand is converted into a TemplateString instance and the operation method is called.

Since all ~ operations result in a string, it doesn’t really matter what type the operands have. It is sufficient if they are primitive FreshMarker types, since only these have a string representation. This is exactly what the new implementation of the TemplateOperation class does.

@Override
public TemplateObject evaluateToObject(ProcessContext processContext) {
  TemplateObject leftValue = left.evaluateToObject(processContext);
  TemplateObject rightValue = right.evaluateToObject(processContext);
  return getTemplateObject(processContext, leftValue, rightValue);
}

private TemplateObject getTemplateObject(ProcessContext processContext, TemplateObject leftValue, TemplateObject rightValue) {
  if (op == Operator.CONCAT && leftValue.isPrimitive() && rightValue.isPrimitive()) {
    return leftValue.asString().operation(op, rightValue.asString(), processContext);
  }
  if (op != Operator.PLUS) {
    return leftValue.operation(op, rightValue, processContext);
  }
  if (leftValue.getModelType().equals(rightValue.getModelType())) {
    return leftValue.operation(op, rightValue, processContext);
  }
  if (leftValue.isPrimitive() && rightValue.isPrimitive()) {
    if (leftValue instanceof TemplateString string) {
      return string.operation(op, rightValue.asString(), processContext);
    }
    if (rightValue instanceof TemplateString string) {
      return leftValue.asString().operation(op, string, processContext);
    }
  }
  return leftValue.operation(op, rightValue, processContext);
}

The getTemplateObject method first checks whether the operator is the ~ operator applied to primitive types. If so, both operands are converted into a TemplateString object and concatenated with a space. After that, all other operators except + are processed on the left operand. For the + operator, the types of the two operands are checked, and if one is a string, the operands are concatenated as string values. Otherwise, the operation is performed as usual on the left operand.

This implementation severely restricts operator overloading because creating a custom, semantically correct implementation of the ~ operator has become practically impossible. On the other hand, all primitive data types can now be combined with the ~ operator without additional implementation effort.

Leave a Comment