Edit this page on GitHub

Right-Associative Extension Methods: Details

The most general signature an extension method can have is as follows:

  • An optional type clause leftTyParams
  • A possibly empty list of using clauses leadingUsing
  • A single parameter leftParam (in an explicit term clause)
  • A possibly empty list of using clauses trailingUsing
  • A name (preceded by the def keyword)
  • An optional type clause rightTyParams
  • An optional single parameter rightParam (in an explicit term clause)
  • Any number of any clauses rest

For example:

  extension [T]                               // <-- leftTyParams
            (using a: A, b: B)(using c: C)    // <-- leadingUsing
            (x: X)                            // <-- leftParam
            (using d: D)                      // <-- trailingUsing
    def +:: [U]                               // <-- rightTyParams
            (y: Y)                            // <-- rightParam
            (using e: E)(z: Z)                // <-- rest

An extension method is treated as a right-associative operator (as in SLS §6.12.3) if it has a name ending in :, and is immediately followed by a single explicit term parameter (in other words, rightParam is present). In the example above, that parameter is (y: Y).

The Scala compiler pre-processes a right-associative infix operation such as x +: xs to xs.+:(x) if x is a pure expression or a call-by-name parameter and to val y = x; xs.+:(y) otherwise. This is necessary since a regular right-associative infix method is defined in the class of its right operand. To make up for this swap, the expansion of right-associative extension methods performs the inverse parameter swap. More precisely, if rightParam is present, the total parameter sequence of the extension method's expansion is:

    leftTyParams  leadingUsing  rightTyParams  rightParam  leftParam  trailingUsing  rest

In other words, we swap leftParams trailingUsing with rightTyParam rightParam.

For instance, the +:: method above would become

  <extension> def +:: [T]
                      (using a: A, b: B)(using c: C)
                      [U]
                      (y: Y)
                      (x: X)
                      (using d: D)
                      (using e: E)(z: Z)

This expansion has to be kept in mind when writing right-associative extension methods with inter-parameter dependencies.

This expansion also introduces some inconsistencies when calling the extension methods in non infix form. The user needs to invert the order of the arguments at call site manually. For instance:

  extension [T](x: T)
    def *:(xs: List[T]): List[T] = ...

  y.*:(ys) // error when following the parameter definition order
  ys.*:(y)

  *:(y)(ys) // error when following the parameter definition order
  *:(ys)(y)

Another limitation of this representation is that it is impossible to pass the type parameters of the def explicitly, (unless called in prefix form). For instance:

  extension (x: Int)
    def *:[T](xs: List[T]): List[T] = ...

  xs.*:[Int](1) // error when trying to set T explicitly

The expansion of right-associative extension methods also affects the order in which contextual parameters can be passed explicitly.

Group extension can also behave unintuitively, in general all extension in a group are extension on the receiver. Except if one of these extensions is a right-associative extension method, in which case that one is an extension on the type of its argument. For instance:

  extension (a: Int)
    def :+(b: Long): Long = ... // extension on Int
    def +:(b: Long): Long = ... // extension on Long