Extension Methods
Extension methods allow one to add methods to a type after the type is defined. Example:
case class Circle(x: Double, y: Double, radius: Double)
extension (c: Circle)
def circumference: Double = c.radius * math.Pi * 2
Like regular methods, extension methods can be invoked with infix .
:
val circle = Circle(0, 0, 1)
circle.circumference
Translation of Extension Methods
An extension method translates to a specially labelled method that takes the leading parameter section as its first argument list. The label, expressed as <extension>
here, is compiler-internal. So, the definition of circumference
above translates to the following method, and can also be invoked as such:
<extension> def circumference(c: Circle): Double = c.radius * math.Pi * 2
assert(circle.circumference == circumference(circle))
Operators
The extension method syntax can also be used to define operators. Examples:
extension (x: String)
def < (y: String): Boolean = ...
extension (x: Elem)
def +: (xs: Seq[Elem]): Seq[Elem] = ...
extension (x: Number)
infix def min (y: Number): Number = ...
"ab" < "c"
1 +: List(2, 3)
x min 3
The three definitions above translate to
<extension> def < (x: String)(y: String): Boolean = ...
<extension> def +: (xs: Seq[Elem])(x: Elem): Seq[Elem] = ...
<extension> infix def min(x: Number)(y: Number): Number = ...
Note the swap of the two parameters x
and xs
when translating the right-associative operator +:
to an extension method. This is analogous to the implementation of right binding operators as normal methods. The Scala compiler preprocesses an infix operation x +: xs
to xs.+:(x)
, so the extension method ends up being applied to the sequence as first argument (in other words, the two swaps cancel each other out). See here for details.
Generic Extensions
It is also possible to extend generic types by adding type parameters to an extension. For instance:
extension [T](xs: List[T])
def second = xs.tail.head
extension [T: Numeric](x: T)
def + (y: T): T = summon[Numeric[T]].plus(x, y)
Type parameters on extensions can also be combined with type parameters on the methods themselves:
extension [T](xs: List[T])
def sumBy[U: Numeric](f: T => U): U = ...
Type arguments matching method type parameters are passed as usual:
List("a", "bb", "ccc").sumBy[Int](_.length)
By contrast, type arguments matching type parameters following extension
can be passed only if the method is referenced as a non-extension method:
sumBy[String](List("a", "bb", "ccc"))(_.length)
Or, when passing both type arguments:
sumBy[String](List("a", "bb", "ccc"))[Int](_.length)
Extensions can also take using clauses. For instance, the +
extension above could equivalently be written with a using clause:
extension [T](x: T)(using n: Numeric[T])
def + (y: T): T = n.plus(x, y)
Collective Extensions
Sometimes, one wants to define several extension methods that share the same left-hand parameter type. In this case one can "pull out" the common parameters into a single extension and enclose all methods in braces or an indented region. Example:
extension (ss: Seq[String])
def longestStrings: Seq[String] =
val maxLength = ss.map(_.length).max
ss.filter(_.length == maxLength)
def longestString: String = longestStrings.head
The same can be written with braces as follows (note that indented regions can still be used inside braces):
extension (ss: Seq[String]) {
def longestStrings: Seq[String] = {
val maxLength = ss.map(_.length).max
ss.filter(_.length == maxLength)
}
def longestString: String = longestStrings.head
}
Note the right-hand side of longestString
: it calls longestStrings
directly, implicitly assuming the common extended value ss
as receiver.
Collective extensions like these are a shorthand for individual extensions where each method is defined separately. For instance, the first extension above expands to:
extension (ss: Seq[String])
def longestStrings: Seq[String] =
val maxLength = ss.map(_.length).max
ss.filter(_.length == maxLength)
extension (ss: Seq[String])
def longestString: String = ss.longestStrings.head
Collective extensions also can take type parameters and have using clauses. Example:
extension [T](xs: List[T])(using Ordering[T])
def smallest(n: Int): List[T] = xs.sorted.take(n)
def smallestIndices(n: Int): List[Int] =
val limit = smallest(n).max
xs.zipWithIndex.collect { case (x, i) if x <= limit => i }
Translation of Calls to Extension Methods
To convert a reference to an extension method, the compiler has to know about the extension method. We say in this case that the extension method is applicable at the point of reference. There are four possible ways for an extension method to be applicable:
- The extension method is visible under a simple name, by being defined or inherited or imported in a scope enclosing the reference.
- The extension method is a member of some given instance that is visible at the point of the reference.
- The reference is of the form
r.m
and the extension method is defined in the implicit scope of the type ofr
. - The reference is of the form
r.m
and the extension method is defined in some given instance in the implicit scope of the type ofr
.
Here is an example for the first rule:
trait IntOps:
extension (i: Int) def isZero: Boolean = i == 0
extension (i: Int) def safeMod(x: Int): Option[Int] =
// extension method defined in same scope IntOps
if x.isZero then None
else Some(i % x)
object IntOpsEx extends IntOps:
extension (i: Int) def safeDiv(x: Int): Option[Int] =
// extension method brought into scope via inheritance from IntOps
if x.isZero then None
else Some(i / x)
trait SafeDiv:
import IntOpsEx.* // brings safeDiv and safeMod into scope
extension (i: Int) def divide(d: Int): Option[(Int, Int)] =
// extension methods imported and thus in scope
(i.safeDiv(d), i.safeMod(d)) match
case (Some(d), Some(r)) => Some((d, r))
case _ => None
By the second rule, an extension method can be made available by defining a given instance containing it, like this:
given ops1: IntOps() // brings safeMod into scope
1.safeMod(2)
By the third and fourth rule, an extension method is available if it is in the implicit scope of the receiver type or in a given instance in that scope. Example:
class List[T]:
...
object List:
...
extension [T](xs: List[List[T]])
def flatten: List[T] = xs.foldLeft(List.empty[T])(_ ++ _)
given [T: Ordering]: Ordering[List[T]] with
extension (xs: List[T])
def < (ys: List[T]): Boolean = ...
end List
// extension method available since it is in the implicit scope
// of List[List[Int]]
List(List(1, 2), List(3, 4)).flatten
// extension method available since it is in the given Ordering[List[T]],
// which is itself in the implicit scope of List[Int]
List(1, 2) < List(3)
The precise rules for resolving a selection to an extension method are as follows.
Assume a selection e.m[Ts]
where m
is not a member of e
, where the type arguments [Ts]
are optional, and where T
is the expected type. The following two rewritings are tried in order:
- The selection is rewritten to
m[Ts](e)
and typechecked, using the following slight modification of the name resolution rules:
-
If
m
is imported by several imports which are all on the nesting level, try each import as an extension method instead of failing with an ambiguity. If only one import leads to an expansion that typechecks without errors, pick that expansion. If there are several such imports, but only one import which is not a wildcard import, pick the expansion from that import. Otherwise, report an ambiguous reference error.Note: This relaxation of the import rules applies only if the method
m
is used as an extension method. If it is used as a normal method in prefix form, the usual import rules apply, which means that importingm
from multiple places can lead to an ambiguity error.
-
If the first rewriting does not typecheck with expected type
T
, and there is an extension methodm
in some eligible objecto
, the selection is rewritten too.m[Ts](e)
. An objecto
is eligible ifo
forms part of the implicit scope ofT
, oro
is a given instance that is visible at the point of the application, oro
is a given instance in the implicit scope ofT
.
This second rewriting is attempted at the time where the compiler also tries an implicit conversion from
T
to a type containingm
. If there is more than one way of rewriting, an ambiguity error results.
An extension method can also be referenced using a simple identifier without a preceding expression. If an identifier g
appears in the body of an extension method f
and refers to an extension method g
that is defined in the same collective extension
extension (x: T)
def f ... = ... g ...
def g ...
the identifier is rewritten to x.g
. This is also the case if f
and g
are the same method. Example:
extension (s: String)
def position(ch: Char, n: Int): Int =
if n < s.length && s(n) != ch then position(ch, n + 1)
else n
The recursive call position(ch, n + 1)
expands to s.position(ch, n + 1)
in this case. The whole extension method rewrites to
def position(s: String)(ch: Char, n: Int): Int =
if n < s.length && s(n) != ch then position(s)(ch, n + 1)
else n
Syntax
Here are the syntax changes for extension methods and collective extensions relative to the current syntax.
BlockStat ::= ... | Extension
TemplateStat ::= ... | Extension
TopStat ::= ... | Extension
Extension ::= ‘extension’ [DefTypeParamClause] {UsingParamClause}
‘(’ DefParam ‘)’ {UsingParamClause} ExtMethods
ExtMethods ::= ExtMethod | [nl] <<< ExtMethod {semi ExtMethod} >>>
ExtMethod ::= {Annotation [nl]} {Modifier} ‘def’ DefDef
In the above the notation <<< ts >>>
in the production rule ExtMethods
is defined as follows :
<<< ts >>> ::= ‘{’ ts ‘}’ | indent ts outdent
extension
is a soft keyword. It is recognized as a keyword only if it appears at the start of a statement and is followed by [
or (
. In all other cases it is treated as an identifier.