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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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.

@hepin1989

This comment has been minimized.

Copy link

hepin1989 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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

bishabosha commented Jan 8, 2020

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

bishabosha and others added 4 commits Jan 9, 2020
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_`.
@bishabosha bishabosha force-pushed the dotty-staging:change-extmethods3 branch from 32ac8be to 74e56a0 Jan 9, 2020
@jdegoes

This comment has been minimized.

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

odersky added 4 commits Jan 7, 2020
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_`.
@odersky odersky force-pushed the dotty-staging:change-extmethods3 branch from c96e7c2 to 14db070 Jan 13, 2020
odersky added 2 commits Jan 13, 2020
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

This comment has been minimized.

Copy link
Contributor

dwijnand commented Jan 13, 2020

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 lampepfl:master Jan 13, 2020
1 check passed
1 check passed
continuous-integration/drone/pr Build is passing
Details
@odersky odersky deleted the dotty-staging:change-extmethods3 branch Jan 13, 2020
@AndreuCodina

This comment has been minimized.

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

This comment has been minimized.

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
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

8 participants
You can’t perform that action at this time.