Skip to content

Commit 122bec2

Browse files
Update SIPs state
1 parent 69420dd commit 122bec2

File tree

1 file changed

+230
-5
lines changed

1 file changed

+230
-5
lines changed
Lines changed: 230 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,233 @@
11
---
2-
title: SIP-54 - Multi-Source Extension Overloads.
3-
status: vote-requested
4-
pull-request-number: 60
5-
stage: design
6-
recommendation: accept
2+
layout: sip
3+
permalink: /sips/:title.html
4+
stage: implementation
5+
status: under-review
6+
title: SIP-54 - Multi-Source Extension Overloads
7+
---
8+
9+
**By: Sébastien Doeraene and Martin Odersky**
10+
11+
## History
12+
13+
| Date | Version |
14+
|---------------|--------------------|
15+
| Mar 10th 2023 | Initial Draft |
16+
17+
## Summary
18+
19+
We propose to allow overload resolution of `extension` methods with the same name but imported from several sources.
20+
For example, given the following definitions:
21+
22+
```scala
23+
class Foo
24+
class Bar
25+
26+
object A:
27+
extension (foo: Foo) def meth(): Foo = foo
28+
def normalMeth(foo: Foo): Foo = foo
29+
30+
object B:
31+
extension (bar: Bar) def meth(): Bar = bar
32+
def normalMeth(bar: Bar): Bar = bar
33+
```
34+
35+
and the following use site:
36+
37+
```scala
38+
import A.*
39+
import B.*
40+
41+
val foo: Foo = ???
42+
foo.meth() // works with this SIP; "ambiguous import" without it
43+
44+
// unchanged:
45+
meth(foo)() // always ambiguous, just like
46+
normalMeth(foo) // always ambiguous
47+
```
48+
49+
## Motivation
50+
51+
Extension methods are a great, straightforward way to extend external classes with additional methods.
52+
One classical example is to add a `/` operation to `Path`:
53+
54+
```scala
55+
import java.nio.file.*
56+
57+
object PathExtensions:
58+
extension (path: Path)
59+
def /(child: String): Path = path.resolve(child).nn
60+
61+
def app1(): Unit =
62+
import PathExtensions.*
63+
val projectDir = Paths.get(".") / "project"
64+
```
65+
66+
However, as currently specified, they do not compose, and effectively live in a single flat namespace.
67+
This is understandable from the spec--the *mechanism**, which says that they are just regular methods, but is problematic from an intuitive point of view--the *intent*.
68+
69+
For example, if we also use another extension that provides `/` for `URI`s, we can use it in a separate scope as follows:
70+
71+
```scala
72+
import java.net.URI
73+
74+
object URIExtensions:
75+
extension (uri: URI)
76+
def /(child: String): URI = uri.resolve(child)
77+
78+
def app2(): Unit =
79+
import URIExtensions.*
80+
val rootURI = new URI("https://www.example.com/")
81+
val projectURI = rootURI / "project/"
82+
```
83+
84+
The above does not work anymore if we need to use *both* extensions in the same scope.
85+
The code below does not compile:
86+
87+
```scala
88+
def app(): Unit =
89+
import PathExtensions.*
90+
import URIExtensions.*
91+
92+
val projectDir = Paths.get(".") / "project"
93+
val rootURI = new URI("https://www.example.com/")
94+
val projectURI = rootURI / "project/"
95+
println(s"$projectDir -> $projectURI")
96+
end app
97+
```
98+
99+
*Both* attempts to use `/` result in error messages of the form
100+
101+
```
102+
Reference to / is ambiguous,
103+
it is both imported by import PathExtensions._
104+
and imported subsequently by import URIExtensions._
105+
```
106+
107+
### Workarounds
108+
109+
The only workarounds that exist are unsatisfactory.
110+
111+
We can avoid using extensions with the same name in the same scope.
112+
In the above example, that would be annoying enough to defeat the purpose of the extensions in the first place.
113+
114+
Another possibility is to *define* all extension methods of the same name in the same `object` (or as top-level definitions in the same file).
115+
This is possible, although cumbersome, if they all come from the same library.
116+
However, it is impossible to combine extension methods coming from separate libraries in this way.
117+
118+
Finally, there exists a trick with `given`s of empty refinements:
119+
120+
```scala
121+
object PathExtensions:
122+
given pathExtensions: {} with
123+
extension (path: Path)
124+
def /(child: String): Path = path.resolve(child).nn
125+
126+
object URIExtensions:
127+
given uriExtensions: {} with
128+
extension (uri: URI)
129+
def /(child: String): URI = uri.resolve(child)
130+
```
131+
132+
The empty refinement `: {}` prevents those `given`s from polluting the actual implicit scope.
133+
`extension`s defined inside `given`s that are in scope can be used, so this trick allows to use `/` with the imports of `PathExtensions.*` and `URIExtensions.*`.
134+
The `given`s must still have different names for the trick to work.
135+
This workaround is however quite obscure.
136+
It hides intent behind a layer of magic (and an additional indirection at run-time).
137+
138+
### Problem for migrating off of implicit classes
139+
140+
Scala 2 implicit classes did not suffer from the above issues, because they were disambiguated by the name of the implicit class (not the name of the method).
141+
This means that there are libraries that cannot migrate off of implicit classes to use `extension` methods without significantly degrading their usability.
142+
143+
## Proposed solution
144+
145+
We propose to relax the resolution of extension methods, so that they can be resolved from multiple imported sources.
146+
Instead of rejecting the `/` call outright because of ambiguous imports, the compiler should try the resolution from all the imports, and keep the only one (if any) for which the receiver type matches.
147+
148+
Practically speaking, this means that the above `app()` example would compile and behave as expected.
149+
150+
### Non-goals
151+
152+
It is *not* a goal of this proposal to allow resolution of arbitrary overloads of regular methods coming from multiple imports.
153+
Only `extension` method calls are concerned by this proposal.
154+
The complexity budget of relaxing *all* overloads in this way is deemed too high, whereas it is acceptable for `extension` method calls.
155+
156+
For the same reason, we do not propose to change regular calls of methods that happen to be `extension` methods.
157+
158+
### Specification
159+
160+
We make two changes to the [specification of extension methods](https://docs.scala-lang.org/scala3/reference/contextual/extension-methods.html).
161+
162+
In the section [Translation of Extension Methods](https://docs.scala-lang.org/scala3/reference/contextual/extension-methods.html#translation-of-extension-methods), we make it clearer that the "desugared" version of the call site may require an explicit qualifier.
163+
This is not strictly a novelty of this SIP, since it could already happen with `given`s and implicit scopes, but this SIP adds one more case where this can happen.
164+
165+
Previously:
166+
167+
> So, the definition of circumference above translates to the following method, and can also be invoked as such:
168+
>
169+
> `<extension> def circumference(c: Circle): Double = c.radius * math.Pi * 2`
170+
>
171+
> `assert(circle.circumference == circumference(circle))`
172+
173+
With this SIP:
174+
175+
> So, the definition of circumference above translates to the following method, and can also be invoked as such:
176+
>
177+
> `<extension> def circumference(c: Circle): Double = c.radius * math.Pi * 2`
178+
>
179+
> `assert(circle.circumference == circumference(circle))`
180+
>
181+
> or
182+
>
183+
> `assert(circle.circumference == qualifierPath.circumference(circle))`
184+
>
185+
> for some `qualifierPath` in which `circumference` is actually declared.
186+
> Explicit qualifiers may be required when the extension method is resolved through `given` instances, implicit scopes, or disambiguated from several imports.
7187
8188
---
189+
190+
In the section [Translation of Calls to Extension Methods](https://docs.scala-lang.org/scala3/reference/contextual/extension-methods.html#translation-of-calls-to-extension-methods), we amend step 1. of "The precise rules for resolving a selection to an extension method are as follows."
191+
192+
Previously:
193+
194+
> 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.
195+
> The following two rewritings are tried in order:
196+
>
197+
> 1. The selection is rewritten to `m[Ts](e)`.
198+
199+
With this SIP:
200+
201+
> 1. The selection is rewritten to `m[Ts](e)` and typechecked, using the following slight modification of the name resolution rules:
202+
>
203+
> - If `m` is imported by several imports which are all on the same nesting level, try each import as an extension method instead of failing with an ambiguity.
204+
> If only one import leads to an expansion that typechecks without errors, pick that expansion.
205+
> If there are several such imports, but only one import which is not a wildcard import, pick the expansion from that import.
206+
> Otherwise, report an ambiguous reference error.
207+
208+
### Compatibility
209+
210+
The proposal only alters situations where the previous specification would reject the program with an ambiguous import.
211+
Therefore, we expect it to be backward source compatible.
212+
213+
The resolved calls could previously be spelled out by hand (with fully-qualified names), so binary compatibility and TASTy compatibility are not affected.
214+
215+
### Other concerns
216+
217+
With this SIP, some calls that would be reported as *ambiguous* in their "normal" form can actually be written without ambiguity if used as extensions.
218+
That may be confusing to some users.
219+
Although specific error messages are not specified and therefore outside the SIP scope, we encourage the compiler implementation to enhance the "ambiguous" error message to address this confusion.
220+
If some or all of the involved ambiguous targets are `extension` methods, the compiler should point out that the call might be resolved unambiguously if used as an extension.
221+
222+
## Alternatives
223+
224+
A number of alternatives were mentioned in [the Contributors thread](https://contributors.scala-lang.org/t/change-shadowing-mechanism-of-extension-methods-for-on-par-implicit-class-behavior/5831), but none that passed the bar of "we think this is actually implementable".
225+
226+
## Related work
227+
228+
- [Contributors thread acting as de facto Pre-SIP](https://contributors.scala-lang.org/t/change-shadowing-mechanism-of-extension-methods-for-on-par-implicit-class-behavior/5831)
229+
- [Pull Request in dotty](https://github.com/lampepfl/dotty/pull/17050) to support it under an experimental import
230+
231+
## FAQ
232+
233+
This section will probably initially be empty. As discussions on the proposal progress, it is likely that some questions will come repeatedly. They should be listed here, with appropriate answers.

0 commit comments

Comments
 (0)