|
| 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