Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix right associativity of extension methods #19203

Conversation

nicolasstucki
Copy link
Contributor

@nicolasstucki nicolasstucki commented Dec 5, 2023

This patch removes the special treatment of extension methods while desugaring. Instead of swapping the arguments of the extension method at definition site, we let the usual desugaring take care of it.

extension (xs: List[Int]) def -:(x: Int) = xs.remove(x)

1 -: List(2, 3) // desugars into List(2, 3).-:(1)
1 :: List(2, 3) // desugars into List(2, 3).::(1)

This implies that the following makes sense now:

List(2, 3).-:(1) // equivalent to the one bellow
-:(List(2, 3))(1)

Note that the parameters are passed in order as in the signature, as they should. This used to break intuition in the previous implementation.

Futhermore, this fixes parameter scoping and dependencies, as it trivially respects the original signature. An example of this is in the following code example, where T is bounded by tuple.type, which would have been out of scope in the previous implementation due to the swapping of parameters.

extension (tuple: Tuple)
  def *:[T >: tuple.type <: Tuple, H](x: H): H *: T = ...

The downside is that this is a breaking change as we change the semantics of the extension method definitions. We need to find a way to migrate from one to the other without breaking code. One option might be to have a different syntax for the new semantics, and deprecate the old one. One possibility would be to use infix def extensions to mark to mark these methods.

One issue encountered in the library is with the IArray.{++:, +:} operations. Luckily, it seems that by rewriting them into the new semantics we get a binary and TASTy compatible signature.

Fixes #19197
Needs SIP

@nicolasstucki nicolasstucki marked this pull request as draft December 5, 2023 15:21
@nicolasstucki nicolasstucki self-assigned this Dec 5, 2023
@nicolasstucki nicolasstucki force-pushed the natural-right-assoc-extention-methods branch from b5c2612 to 70dcc34 Compare December 5, 2023 15:48
This patch removes the special treatment of extension methods while
desugaring. Instead of swapping the arguments of the extension method
at definition site, we let the usual desugaring take care of it.

```scala
extension (xs: List[Int]) def -:(x: Int) = xs.remove(x)

1 -: List(2, 3) // desugars into List(2, 3).-:(1)
1 :: List(2, 3) // desugars into List(2, 3).::(1)
```

This implies that the following makes sense now:
```scala
List(2, 3).-:(1) // equivalent to the one bellow
-:(List(2, 3))(1)
```
Note that the parameters are passed in order as in the signature,
as they should. This used to break intuition in the previous implementation.

Futhermore, this fixes parameter scoping and dependencies, as it trivially
respects the original signature. An example of this is in the following
code example, where `T` is bounded by `tuple.type`, which would have been
out of scope in the previous implementation due to the swapping of parameters.

```scala
extension (tuple: Tuple)
  def *:[T >: tuple.type <: Tuple, H](x: H): H *: T = ...
```

The downside is that this is a breaking change as we change the semantics
of the extension method definitions. We need to find a way to migrate from
one to the other without breaking code. One option might be to have a
different syntax for the new semantics, and deprecate the old one. One
possibility would be to use `infix def` extensions to mark to mark these
methods.

One issue encountered in the library is with the `IArray.{++:, +:}`
operations. Luckily, it seems that by rewriting them into the new
semantics we get a binary and TASTy compatible signature.

Fixes scala#19197
@dwijnand
Copy link
Member

dwijnand commented Dec 7, 2023

I think the previous behaviour is a bug, so it's a behaviour fixing change.

But also:

extension (xs: List[Int]) def -:(x: Int) = xs.remove(x)

1 -: List(2, 3) // desugars into List(2, 3).-:(1)
1 :: List(2, 3) // desugars into List(2, 3).::(1)

Could you make sure it desugars properly to:

        val x$1: Int = 1
        List(2, 3).-:(x$1)

like we do for infix class methods, as I think changing the order of evaluation of the left and right-hand side would also be a bug. I.e. some test where you use side-effects to assert the execution order.

@nicolasstucki
Copy link
Contributor Author

       val x$1: Int = f
      List(2, 3).-:(x$1)

This is already the current desugaring when the argument is knownt to be pure. I used a simpler example to only have to show how the method is called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Extension method with dependent type bounds
2 participants