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

New syntax for collective extension methods #7917

Merged
merged 12 commits into from
Jan 13, 2020

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jan 7, 2020

It's now:

extension listOps on [T](xs: List[T]) with {
  def second = xs.tail.head
  def third: T = xs.tail.tail.head
}

instead of

given listOps: [T](xs: List[T]) extended with {
  ...
}

Allow

    def (c: Circle).circumference: Double

alongside

    def (c: Circle) circumference: Double

The syntax with '.' is preferred for normal methods, which have names
starting with a letter and which are not declared @infix. Right now,
this preference is not enforced.
@odersky
Copy link
Contributor Author

odersky commented Jan 7, 2020

@bishabosha There's a bunch of semanticdb tests that need to be updated. Can you take care of that? Thanks!

@odersky
Copy link
Contributor Author

odersky commented Jan 7, 2020

Like its ancestor #7914, this PR changes docs as well as implementation. To avoid confusion, it should be merged just before the next release. so that we can keep the two in sync.

@odersky odersky added this to the 0.22 Tech Preview milestone Jan 7, 2020
@odersky
Copy link
Contributor Author

odersky commented Jan 7, 2020

Background and Motivation

Originally, Dotty had only regular extension methods (their syntax just got upgraded in #7914). These extension methods are great since they can be defined everywhere and they can be abstract. That aspect enables in particular the nice expression of infix methods in type classes.

But there was a usability problem: If several concrete extension methods are defined together their type parameters and leading value parameter have to be repeated for each method. The current solution to extension methods with implicit classes does this much better.

Until now we tried several ways to carve out a special case in the syntax for given instances that only define extension methods. But there was always the problem of "false friends": given instances are about types whereas collective extensions describe parameters of extension methods. The two look similar enough to be confusing.

That's why I finally bit the bullet in proposing a completely separate syntax for collective extension methods. It makes things a lot clearer. My previous hesitation to claim extension as a keyword can be worked around by making extension and of soft keywords that are only recognized in tandem.

The extension syntax looks quite similar to what has been added recently to Dart. The main difference is that we still insist on naming the first parameter, whereas Dart repurposes this for referencing this parameter. This choice was made so that normal and collective extension methods follow the same rules.

@He-Pin
Copy link

He-Pin commented Jan 7, 2020

Refs dart-lang/language#41 , it's really glad to see Dotty make this move,Dart do have some fancy stuff

@soronpo
Copy link
Contributor

soronpo commented Jan 8, 2020

What happens when you refer to this inside an extension method, both in top-level, and inside a class/object?
Top level:

extension IntOps of (i : Int) with {
  def foo : Int = this.i //Can we refer to `this`?
}

Inside class:

class Foo {
  val i : Int = 0
  extension IntOps of (i : Int) with {
    def foo : Int = this.i //Does `this.i` refer to `Foo.i`, `IntOps.i` or generate an error?
  }
} 

@odersky
Copy link
Contributor Author

odersky commented Jan 8, 2020

@soronpo It's explained by the expansion: this refers to the extension object itself.

@bishabosha bishabosha assigned odersky and unassigned bishabosha Jan 8, 2020
@bishabosha
Copy link
Member

looks like one of the tests ran by intent failed in the community build factor10/intent#44

bishabosha and others added 4 commits January 9, 2020 13:56
It's now:
```scala
extension listOps of [T](xs: List[T]) with {
  def second = xs.tail.head
  def third: T = xs.tail.tail.head
}
```
instead of
```scala
given listOps: [T](xs: List[T]) extended with {
  ...
}
They now start with `extension_` instead of `given_`.
@jdegoes
Copy link

jdegoes commented Jan 11, 2020

I like this. I'd prefer if all the main uses of implicits had totally different syntax:

  • type class instances
  • extension methods
  • automatic conversions
  • proofs
  • context threading ("reader")

It leads to a different style of learning and teaching.

A developer knows when they need an extension method (more or less). They know when they need a low-friction way to convert between types. They know when they need to prove something to the compiler (e.g. something is a subtype of something else). Etc.

Even though all these things use the same mechanism, they are all different use-cases and most programmers think and learn in a use-case-oriented way.

Indeed, proliferation of use-case specific features is not considered "complexity" by most programmers, even though objectively it inflates the size of the grammar (see C#, Kotlin, etc.).

Allow

    def (c: Circle).circumference: Double

alongside

    def (c: Circle) circumference: Double

The syntax with '.' is preferred for normal methods, which have names
starting with a letter and which are not declared @infix. Right now,
this preference is not enforced.
It's now:
```scala
extension listOps of [T](xs: List[T]) with {
  def second = xs.tail.head
  def third: T = xs.tail.tail.head
}
```
instead of
```scala
given listOps: [T](xs: List[T]) extended with {
  ...
}
They now start with `extension_` instead of `given_`.
Rearrange doc pages so that we can merge before the next release.
"extension on" is more precise. The thing after the "of/on" is a parameter.
The definition is not an extension "of" this parameter (what does that even
mean?), but rather an extension of the parameter's underlying type.

Note that Dart also uses "extension on".
@dwijnand
Copy link
Member

Btw, "extension of" and "extension on" work equally well. But seeing as Dart uses "extension on", copying them keeps future, poly Scala/Dart programmers sane. 😄

@odersky odersky merged commit 924bcd9 into scala:master Jan 13, 2020
@odersky odersky deleted the change-extmethods3 branch January 13, 2020 17:50
@AndreuCodina
Copy link

AndreuCodina commented Feb 6, 2020

For me, it would be great if the unnnecessary boilerplate is removed because:

  • I don't need an explicit reference to the extended type.
  • I don't need a name for the extension. You can add it with one keyword or just reference it with some keyword.

The ideal language is easily readable.

Now:

extension on (x: Int)
  def isPositive(): Boolean =
    x > 0

My favourite syntax:

extension Int
  def isPositive(): Boolean =
    this > 0

Important: this is the extended object, and with this you can access to other extended methods too.

My proposal with real code is much less verbose than current implementation.

And personally, I prefer extend because you read it "extend Int" instead of "extension for/on Int".

extend Int
  def isPositive(): Boolean =
    this > 0

@seigert
Copy link

seigert commented Feb 7, 2020

I wonder if new syntax will allow for (generalized) type constraints? For example:

extension eitherThrowableOps on [A, E <: Throwable](e: Either[E, A]) {
  def toTry: Try[A] = e.fold(Try.failure, Try.success)
}

or (less preferred)

extension eitherThrowableOps on [A, E](e: Either[E, A])(using ev: E <:< Throwable) {
  def toTry: Try[A] = this.fold(Try.failure, Try.success)
}

Edit: I found in extension methods docs that at least context bounds is allowed for general extension methods, but for collective extensions only example is

extension on [T](xs: List[T])(using Ordering[T]) {
  def largest(n: Int) = xs.sorted.takeRight(n)
}

Is use of context bound is possible there too?

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

Successfully merging this pull request may close these issues.

8 participants