Skip to content

Commit 983f56a

Browse files
committed
SIP-54: Multi-Source Extension Overloads.
1 parent 7ce04f6 commit 983f56a

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
layout: sip
3+
permalink: /sips/:title.html
4+
stage: implementation
5+
status: waiting-for-implementation
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+
regularMeth(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+
The only other 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+
### Problem for migrating off of implicit classes
119+
120+
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).
121+
This means that there are libraries that cannot migrate off of implicit classes to use `extension` methods without significantly degrading their usability.
122+
123+
## Proposed solution
124+
125+
We propose to relax the resolution of extension methods, so that they can be resolved from multiple imported sources.
126+
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.
127+
128+
Practically speaking, this means that the above `app()` example would compile and behave as expected.
129+
130+
### Non-goals
131+
132+
It is *not* a goal of this proposal to allow resolution of arbitrary overloads of regular methods coming from multiple imports.
133+
Only `extension` method calls are concerned by this proposal.
134+
The complexity budget of relaxing *all* overloads in this way is deemed too high, whereas it is acceptable for `extension` method calls.
135+
136+
For the same reason, we do not propose to change regular calls of methods that happen to be `extension` methods.
137+
138+
### Specification
139+
140+
From the [specification of 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."
141+
142+
Previously:
143+
144+
> 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.
145+
> The following two rewritings are tried in order:
146+
>
147+
> 1. The selection is rewritten to `m[Ts](e)`.
148+
149+
With this SIP:
150+
151+
> 1. The selection is rewritten to `m[Ts](e)` and typechecked, using the following slight modification of the name resolution rules:
152+
>
153+
> - 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.
154+
> If only one import leads to an expansion that typechecks without errors, pick that expansion.
155+
> If there are several such imports, but only one import which is not a wildcard import, pick the expansion from that import.
156+
> Otherwise, report an ambiguous reference error.
157+
158+
### Compatibility
159+
160+
The proposal only alters situations where the previous specification would reject the program with an ambiguous import.
161+
Therefore, we expect it to be backward source compatible.
162+
163+
The resolved calls could previously be spelled out by hand (with fully-qualified names), so binary and TASTy compatibility are not affected.
164+
165+
## Alternatives
166+
167+
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".
168+
169+
## Related work
170+
171+
This section should list prior work related to the proposal, notably:
172+
173+
- [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)
174+
- [Pull Request in dotty](https://github.com/lampepfl/dotty/pull/17050) to support it under an experimental import
175+
176+
## FAQ
177+
178+
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)