Extension Methods Revisited

Dotty currently uses three syntactically different schemes for extension methods

  1. Single methods with prefix parameters such as def (x: T).f(y: U) or def (x: T) op (y: U) .
    These methods can be abstract, can override each other, etc, which is crucial for infix ops
    in type classes.
  2. Given instances defining (only) extension methods with syntax extension ops { ... }
  3. Collective extension methods with syntax extension on (x: T) { ... }

While they cover a large set of use cases, the different syntaxes feel complex and are an impediment to learning. Ideally, we should have one syntax that covers all use cases. This is done by
https://github.com/lampepfl/dotty/pull/9255 which proposes an alternative design that unifies (1) and (3) and all but eliminates the need for (2).

Here is the revised doc page for new design. There are already quite a few comments on #9255 which are relevant for a discussion here.

18 Likes

This looks much easier to work with :+1:

1 Like

I think this looks much simpler than previously! What would be the plan for introducing this change? We are working on the scalameta parser that will be used in worksheets, scalafmt and scalafix, so it would be awesome to synchronize the work. We already implemented parsing the extension methods, but from what I see the changes shouldn’t be too hard.

3 Likes

The change should be merged very soon. Then it would be part of 0.26, to be released in about 4 weeks.

1 Like

A question just popped to my mind. Maybe my use case is a bit out of scope. But I’d like to discuss it.

It is currently possible to import the members of an object (or any stable path):

object Foo {
  def bar = ...
}

object Quux {
  import Foo.bar
  // ... use `bar`
}

If Foo.bar was defined as an extension method, would I be able to import it (so that wouldn’t need to write Foo)?

object Foo

extension (foo: Foo.type) def bar = ...

object Quux {
  import Foo.bar
  // ... use `bar`
}
1 Like

Tagging along with this, if not, would there be any other way to bring bar in to scope directly?

Yes, of course. There’s an example for this in the doc page.

1 Like

You would always have to write Foo, no? Either Foo.bar or extension_bar(Foo).

Yes, that’s true.

Whenever there’s a discussion on the topic of extension methods, I can’t help myself and have to share my idea about it :slight_smile:
How about we allow any method to be used as extension method, if it has the last parameter block singleton (where the receiver would be the only parameter in the block)?

It could look like this:

case class A(...)
def f(b: B, c: C)(a: A): D = ...
a.f(b, c) // de-sugars to f(b, c)(a)

The advantage of this is that it makes the very same methods methods easily chainable, in both scenarios:

  • using . as method application: sequence.map(plus1).filter(isEven)
  • using andThen (or >>> ) as method composition: map(plus1) andThen filter(isEven)
class Sequence[A](...)
...
def map(f: A => B)(s: Sequence[A]): Sequence[B] = ???
def filter(p: A => Boolean)(s: Sequence[A]): Sequence[A] = ???
...
sequence.map(plus1)
// de-sugars to `map(f)(sequence)`, since
// * `Sequence` doesn't have a member `map` and
// * `map(...)(s: Sequence[A])` is in scope

sequence.map(plus1).filter(isEven)
// de-sugars to `filter(isEven)(map(f)(sequence))`

// and these same function can also be used on their own very nicely, especially with chaining
map(plus1) andThen filter(isEven)

Currently we have to nest method calls in parentheses, which is cumbersome, e.g.
filter(isEven)(map(f)(sequence))

I think doing extensions methods as sketched here, would be simpler, less magical and even more flexible.
(previous posts 1 2 3)

The upside-down-ness of f(a, b)(c) being called as c.f(a, b) is not particularly pleasing.

It’s even less pleasing when one considers that implicit parameter blocks tend to go at the end, so it might actually be def f(a: A, b: B)(c: C)(implicit foo: Foo), which means the c is coming out of the middle.

I like using an explicit keyword, having an explicit desugaring, and keeping everything in the exact same order.

5 Likes

One of the main design goals of the current proposal was to facilitate type class definitions. This was done by making extension methods different than other methods, in that they can also come from the implicit scope. It’s probably unrealistic to leak all methods which happen to have a single parameter in the last parameter list into implicit scopes.

2 Likes

I believe the “any method can be used as an extension method” idea is implemented in D. It looks attractive in its simplicity. But I believe it will lead to too many different ways to write code. It would be the same mistake we made when allowing infix or dot application for any method, only worse. Besides,
it would not work for typeclasses, as @LPTK says. For instance if we take the Monoid type class in the docs, x.combine(y) would be rewritten to combine(x)(y) but that leads nowhere. You have to expand to
summon[Monoid].extension_combine(x)(y) instead.

8 Likes

For the record, first language where I’ve seen something like this was in Koka :slight_smile:

Anyway, my “proposal” was quite vague, so there have been some misunderstandings about how this scheme would work. Please let me clarify them.

I am optimistic that this feature would not lead to contentions about how to write code. For example, in F#, if we have functions

let map f xs = ...
let filter p xs = ...

let xs = ...
let f = ...
let p = ...

map f (filter p xs)        // (1)
xs |> filter p |> map f    // (2)

there are more than one way how to apply the functions to their arguments ((1), (2)). But in my experience, people don’t argue about this, because in vast majority of cases, it is obvious which way is better or it doesn’t matter at all. We could think of other examples in other languages (Haskell’s $/&, …). Analogously, I would argue, that Scala developers would handle having available both (1): map(f)(filter(p)(xs)) and (2): xs.filter(p).map(f) just fine.

The advantage would be visible for example in xs.filter(p).sortBy(ord).drop(10).map(f). Using the (1) style just would not be acceptable, because the level of ( ) nesting would be cumbersome. The . now serves the role of some sort of function application operator (like F#/OCaml’s |> or Haskell’s $/&). I find that a great complement to the traditional role of . as member selection operator, honoring the Scala ethos of uniting the OO and functional paradigms.


This scheme could work well with type classes too:

trait SemiGroup[T]:
  def combine (y: T)(x: T): T

trait Monoid[T] extends SemiGroup[T]:
  def unit: T


given Monoid[Int]:
  def combine (y: Int)(x: Int): Int = x + y
  def unit: Int = 0


def combineAll[T](using Monoid[T])(xs: List[T]): T =
  // members of an implicit parameter are imported into scope, so that's why mere `unit` and `_.combine(_)` work
  xs.foldLeft(unit)(_.combine(_))


List(1, 2, 3).combineAll

That shouldn’t be necessary:

The only necessary additional feature would be to import members of implicit parameters.
But I would argue that is justifiable, since implicit parameters are special, they represent a proof (that integers and + form a Monoid, in our example), they propagate themselves automatically and sometimes we don’t even want to name them. And so I think importing their members (like unit and combine), to have them readily available, is warranted.

An improvement like this would not be possible in the Scala 2.x branch, because the impact would not be small. But what better time to consider such change than now when we’re before the 2 -> 3 transition?

1 Like