-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Unified extension methods #9255
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
Conversation
I know it's late in the game for language changes. I started thinking about this while revising the material for the ProgFun MOOC to use Scala 3. I wanted to introduce extension methods early, but found that the overall syntax was too complex to admit a simple introduction. Each of the three parts are OK, but together they are confusing. Why three different ways to do things? There's no good explanation since there is no good reason, after all. |
Three particular complications for the implementation are:
|
At first glance, I love it! This addresses the two reservations I had about extension methods:
object List:
extension [T](xs: List[List[T]]) def flatten: List[T] = ...
object Future:
extension [T](xs: Future[Future[T]]) def flatten: Future[T] = ... It's natural to want to abstract over them, and like with regular methods the answer is to use a trait with an abstract method inside: trait Flattenable[F[_]]:
extension [T](xs: F[F[T]]) def flatten: F[T] At this point, the given mechanism needs to be introduced, but it's not such a big conceptual leap anymore: in the code, one only has to wrap the existing definitions of extension methods without changing them in any way: object List:
given Flattenable[List]:
extension [T](xs: List[List[T]]) def flatten: List[T] = ...
... (previously, this would have required switching from an |
In the old design there was a limitation that the extended and extendee couldn't have different type arguments. extension [T](foo: Foo[T])
def +[R](rightFoo : Foo[R]) = ??? extension [T](foo: Foo[T]):
def +[R](rightFoo : Foo[R]) = ???
def *[R](rightBar : Bar[R]) = ??? |
I have been looking into syntax highlighting for The solution proposed in this PR would make syntax highlighting trivial for |
We used to have names for the collective extension as in extension Ops1 on [T](x: List[T]):
def h: T = x.head
extension Ops2 on (x: List[Int]):
def sm: Int = x.sum We also use those names to disambiguate extensions in the reflection interface to https://github.com/lampepfl/dotty/blob/master/library/src/scala/tasty/Reflection.scala#L541. Though for that case, we could work around it if |
We currently have somthing like trait R:
type X
type Y
extension Xops on (x: X):
def i: Int = 1
extension Yops on (y: Y):
def i: Int = 2 Which would become trait R:
type X
type Y
extension (x: X) def i: Int = 1 // def extension_i(x: X): Int = 1
extension (y: Y) def i: Int = 2 // def extension_i(y: Y): Int = 2 But after erasure, both This could be OK if we allow extension methods to be found without given imports for trait R:
type X
type Y
object X:
extension (x: X) def i: Int = 1 // def extension_i(x: X): Int = 1
object Y:
extension (y: Y) def i: Int = 2 // def extension_i(y: Y): Int = 2 val r: R = ...
val x: r.X = ...
x.i |
trait R:
type X
type Y
extension Xops on (x: X):
def i: Int = 1
extension Yops on (y: Y):
def i: Int = 2 can be expressed like this: trait R:
type X
type Y
given Xops as AnyRef:
extension (x: X) def i: Int = 1
given Yops as AnyRef:
extension (y: Y) def i: Int = 2 We could allow to drop the initial type trait R:
type X
type Y
given Xops as:
extension (x: X) def i: Int = 1
given Yops as:
extension (y: Y) def i: Int = 2 |
@soronpo The restriction that there may be only one type parameter section is still present (found in the section "Collective Extension Methods"). it probably should be moved to "Generic Extensions". |
|
||
### Collective Extensions | ||
|
||
Sometimes, one wants to define several extension methods that share the same | ||
left-hand parameter type. In this case one can "pull out" the common parameters into the extension instance itself. Examples: | ||
left-hand parameter type. In this case one can "pull out" the common parameters into | ||
a single extension and enclose all methods in braces or an indented region following a '`:`'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we also support collective extension with { ... }
as in extension (x: Int) { def ... }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we do support that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should mention that the following are equivalent
extension (x: Int) {
def f(y: Int) = x + y
}
extension (x: Int):
def f(y: Int) = x + y
This would help users understand the scope of the extension
.
This seems to make sense, overall. It seems to be a nice simplification from the user point of view. @nicolasstucki For the erasure issues, can they be circumvented by adding a |
Dummy implicits could work, but it does not scale well and there are the performance implications. |
Maybe |
Looks awesome! Where would
|
@japgolly Yes. Modifiers precede the |
Ah sorry, I see I should've been a bit clearer. So to confirm these are ok:
and these are not, right?
|
Also would it not be better to translate extension methods to |
edfc820
to
3ba9abe
Compare
Users should be able to refer to extension names, for instance in order to disambiguate calls, or debug type inference. That's why we wanted a user-accessible name for them.
They are all illegal since the givens would already produce a double definition error. |
3ba9abe
to
546eeb0
Compare
I would opt for |
The loss of the very short syntax for def [A, B](fa: F[A]).map(f: A => B): F[B] is regrettable... trait Functor[F[_]] {
def [A, B](fa: F[A]).map(f: A => B): F[B]
}
def plus[F[_]](f: F[Int])(using F: Functor[F]) =
// prefix form
F.map(f)(_ + 1)
// infix form
f.map(_ + 1) The proposed change eliminates this convenience and forces one to write copies of extension methods to enable prefix form without trait Functor[F[_]] {
final def map(fa: F[A])(f: A => B): F[B] = fa.map(f)
extension[A, B](fa: F[A]) def map(f: A => B): F[B]
}
def plus[F[_]](f: F[Int])(using F: Functor[F]) =
// prefix form doesn't look very good...
F.extension_map(f)(_ + 1)
// prefix form of boilerplate:
F.map(f)(_ + 1)
// infix form
f.map(_ + 1) |
6ff952d
to
a60ca32
Compare
@neko-kai It's true that the previous short extension method syntax was more concise, and was quite elegant in some cases. But in the use cases I have seen so far, collective extension methods turned out to be just as important as individual ones, and having two different syntaxes is problematic for learning. While being more verbose, the new syntax has also some advantages: extension [A, B](fa: F[A])
def map(f: A => B): F[B] Here, we keep the invariant that method names always follow a About F.extension_map(f)(_ + 1) is actually clearer than F.map(f)(_ + 1) Adding the
Here, the recursive call |
If a candidate can be both a conversion and an extension, which happens if it is overloaded, and the extension is not applicable, try the conversion.
Check files should be only added if they test a tricky error message, typically one with lots of configurable parts. A simple string error message should never lead to a check file. Having too many check file just causes unnecessary friction for me and the other maintainers.
If so, what does |
It's the outer |
There's will probably be a tendency to put a `:` in a collective extension by analogy to given and object, trait etc. We don't need to be picky about this.
Maybe it's best to allow an optional |
My 2 cents:
(Btw, I really like this new way of declaring extension methods, thank you.) |
@odersky def outerlocal = {
def localfun: Int = {
def localfun(i: Int): String = {
if (i == 0) "x" else (1 + localfun).toString
}
localfun(util.Random.nextInt()).toInt
}
localfun
} |
Dotty currently uses three syntactically different schemes for extension methods
def (x: T).f(y: U)
ordef (x: T) op (y: U)
.These methods can be abstract, can override each other, etc, which is crucial for infix ops
in type classes.
extension ops { ... }
extension on (x: T) { ... }
This PR proposes another scheme that unifies (1) and (3) and all but eliminates the need for (2).
The simplification is achieved by redefining many fundamentals of extension methods. In a sense,
the price we pay for user-facing simplifications is a more complex implementation scheme. But that's
probably a price worth paying.
The first commit is docs only. I updated the doc pages for extension methods, type classes, and
the grammar.