Pre SIP: Better desugarring of extension methods

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)

3 Likes

For info: We had that at some point before settling on the current scheme.

It won’t solve the gymnastics problem since the parser just sees

x.show

It does not know whether this is an extension method or not. It also makes name resolution more complicated since there are now two competing names to look up. What are the rules to do that?

The only way to solve the gymnastics problem is to rule out right-associative regular methods and only allow right-associative extension methods. But it will be a long way to get there.

I was talking about gymnastics only to describe the parameter shuffling at the declaration site of right associative extension methods, not the transformation at use site, which the new naming scheme does remove

Isn’t that already a problem when there are both an extension method and a regular method available, how is that different ?

Yes, no too different.

The other problem with the $extension name mangling scheme (and the one which eventually sunk the idea) was that we need a way to disambiguate extensions, calling them with a given prefix. We don’t want to use mangled names for that.

I don’t understand why you mean, could you give an example ?

This is the case where you explicitly call the method not as an extension, e.g.

trait Show[-T]:
  extension (t: T) def show: String

def doShow[A: Show](a: A) = summon[Show[A]].show(a)

Could we instead use a “semi mangled” name instead:
something like show_extensionMethod or something like that

With the idea that it is legal, but strongly discouraged, to define your own something_extensionMethod

2 Likes

IIUC the usual trick is to include at least one $ character in compiler-generated names, which prevents overrides from Java, and makes it a little trickier in Scala. :slight_smile:

Mangling, I know, it is what I initially proposed, but as you can see above, it has downsides in this case