The way I see it, there are currently two issues with extension methods:
- They pollute non-extension namespace
- Some unintuitive gymnastics are needed to make them work when right-associative
Namespace pollution
trait Showable[T]:
extension (x: T)
def show: String
We can now use it like so, even though this is not communicated at all by the declaration:
show(x)
On its own, this is not so bad, at least it allows us to do the following, right ?
given Showable[Int] with
override def show(x: Int) = x.toString
Well, no it doesn’t (scastie), forcing us to write the following:
given Showable[Int] with
extension (x: Int)
override def show = x.toString
(To be clear: I think allowing it as-is would be going in the opposite direction I’m advocating for, but showcases that there is already precedent for making a hard distinction between extension and non-extension methods.)
Given all of this, it would be clearer to write the above as:
trait Showable[T]:
def show(x: T): String
extension (x: T)
final def show: String = show(x)
given Showable[Int] with
override def show(x: Int) = x.toString
If show
was a complicated method, it might be nice not to have it in the middle of an extension block, which tends to look a bit cluttered and confusing.
Sadly, this does not work (scastie)
Ambiguous overload. The overloaded alternatives of method show in trait Showable with types
(x: T): String
(x: T): String
both match arguments ((x : T))
because the extension method already declares that non-extension version, as we have seen before.
Gymnastics
Because the parser swaps the parameters of right associative methods at use-cite, we need to preemptively swap them ahead of time.
The parser does not know yet if these are extension methods or not, as they look syntactically identical.
Proposed Solution
Extension methods like
extension (x: T)
def show: String = body
def <:(l: List[T]): Boolean = ???
Are desugarred into
<extension> def show$extension(x: T): String = body
<extension> def <:$extension(x: T)(l: List[T]): Boolean = ???
As before, methods ending in :
are parsed right-associatively, and get their parameter swapped.
But methods ending in :$extension
are only parsed right-associatively, no parameter swaps.
This allows us to do our initial:
trait Showable[T]:
def show(x: T): String
extension (x: T)
final def show: String = show(x)
As it gets desugarred to:
trait Showable[T]:
def show(x: T): String
<extension> final def show$extension(x: T): String = show(x)
There is no name clash anymore between the two methods, in particular, we can now be certain the second method is not recursive.
Backwards compatibility
We can identify methods previously generated by noticing those all have an <extension>
tag, but not a name containing $extension
, we can therefore handle these differently when needed.
For example an <extension> foo
can be called as x.foo
and foo(x)
, but an <extension> foo$extension
can only be called as x.foo
(and foo$extension(x)
).
(There might very well be other concerns, help is welcome)