Proposal to add Extension Methods to the language

We are considering adding extension methods to Scala 3 in the form they are currently described on the Dotty doc page.

Introductory example:

    case class Circle(x: Double, y: Double, radius: Double)

    def (c: Circle) circumference: Double = c.radius * math.Pi * 2

The main novel feature of extension methods is their integration with implicit search:

  • An extension method is applicable if it is a member of some implied instance at the point of the application.

This allows a clean notation for type classes, without the need to import decorator classes to get infix operations.

Motivation

Extension methods are a more straightforward and simpler alternative to implicit classes. The proposed
design integrates well with implicit search, enabling a pleasing and lightweight notation for type classes.

Discussion

The proposal has already undergone extensive discussions on the PR with 165 comments posted. The discussion focussed mostly on the syntax. In fact, several alternative syntactic forms were implemented and tried out before settling on the one in the current proposal. A popular alternative syntax is to use C# style extension methods with a this modifier in front of the “left” parameter.

E.g.

    def < (this x: String)(y: String) = ... 

instead of the now implemented

    def (x: String) < (y: String)

It’s not an easy choice. I was for while convinced that C# style was the way to go, to a point where I did a complete implementation and gave several talks last fall about the new feature with this syntax. But the more I used it and talked about it, the more the syntax felt awkward to me, as something that does not quite fit and that requires too many explanations. One case whether this is particularly blatant are right binding operators. E.g.

    def +: (this xs: Iterable[Elem])(x: Elem): Iterable[Elem]

vs

    def (x: Elem) +: (xs: Iterable[Elem]): Iterable[Elem]

That’s why I changed in the end to the current syntax.

If people would like to discuss syntax here and maybe propose a new one, it would be good to first consult the comments in the PR to see what was discussed and discarded already.

16 Likes

Big :+1: from the peanut gallery – while I originally found the new syntax a little off-putting, it’s grown on me a lot. I think it’s intuitive and clear once you get over the initial “Ack! New and scary!” hurdle, and I think it’ll be nicely teachable…

What about overloading and overriding?

I think this syntax is the the way to go: write the definition in the same order as the usage.

Indeed, there is a generalization of this that allows interspersing of arguments and method names that perhaps we don’t actually want, but would allow less contorted creation of DSL syntax.

def (s: Seq) from (start: Int) until (end: Int) = ...

to replace xs.slice(3, 9) with xs from 3 until 9.

Anyway, I don’t necessarily advocate for this much generality (probably a pain to parse, and then there is ambiguity in a R b S c whether it’s a single method R(_)S(_) or the S method on the result of a R b.

However, if the extension method follows this form, I think it fits into the conceptual space of such things much more nicely than if we have magical new syntax.

5 Likes

Unless you’re calling a right-associative operator as a normal method. Currently the order of operands in a method definition matches that of a method call but with this new syntax for extension methods it matches the order in an operator application. This makes extension method definitions different from regular methods:

class C {
  // Normal method definition, argument list on the right (method style)
  def :: (x: Elem): C = ...
}

// Extension method definition, argument list on the left (operator style)
def (x: Elem) :: (xs: C): C - ...
2 Likes

I don’t know enough to give the official answer, but IMO trying to overload an existing method with an extension method is kind of playing with fire. At least from a coding standards POV I would say, “Don’t do that”.

(If you mean, what happens if there are two competing overloads of the same extension method, I would hope that the compiler spits out an ambiguity error, but I dunno…)

One could always allow the following as valid alternatives:

def (x: Elem) :: (xs: C): C = ...
def (xs: C).::(x: Elem): C = ...

so you can define it either way in addition to being able to use it either way.

I’m finding extension methods really, really useful. They totally rock.

The one fly in the ointment is in supplying type parameters. Having them after the operation name causes a strange forward-reference into the left-most argument list.

I would add a +1 to @Ichoran’s suggestion about multiple method names. Possibly with their own type params and given clauses.

As a point of style, I’m still finding that it is useful to separate out the formal and “sugared” form of APIs. So the typeclass or tagless DSL is typically in the normal form, but a single implied Syntax entity contains all the symbolic operators and infix extension methods. It’s sometimes difficult to see how to force given arguments for infix notation. Where as the raw form you can just say M.foo to access it directly.

Immensely useful feature, glad to see it’s getting special language support.

To throw some more syntax from the peanut gallery. I really like Kotlin’s syntax for extension methods because it is minimalist and restrictive.

def Circle.circumference: Double = this.radius * math.Pi * 2

A disadvantage it that it shadows the existant this. Could this be solved similar to OuterClass.this vs this in a nested class?

But the implemented syntax is perfectly fine.

(Apologies if this has been considered and discarded, I could not find a discussion about this variant)

How would this deal with extension methods like flatten?

    def (xss: List[List[T]]) flatten [T]: List[T] = 
      xss.foldRight(Nil)(_ ++ _)

If it’s important to you to separate the syntax from the typeclass you could still use a pattern like this.

trait Monoid[A] {
  def combine(x: A, y: A): A
  def empty: A
}
object syntax {
  class MonoidSyntax[A](monoid: Monoid[A]) {
    def (x: A) |+| (y: A) = monoid.combine(x, y)
  }
  implicit def MonoidSyntax[A: Monoid]: MonoidSyntax[A] = new MonoidSyntax(implicitly[Monoid[A]])
}

But I personally like that all the extra syntax stuff is no longer necessary.

@Jasper-M This is what I’ve been doing. It is a much cleaner separation of concerns IMHO. And users can choose to import or not import the syntax. You can do it like:

@FunctionalInterface
trait Semigroup[S] {
  def append(lhs: S, rhs: S): S
}

/** Syntax for working with semigroups. */
implied SemigroupSyntax {
  /** Apply the semigroup infix. */
  inline def (lhs: S) |+| [S](rhs: S) given (S: Semigroup[S]): S = S.append(lhs, rhs)
}

Overloading existing methods with extension methods is vital and I use it a lot:

def * (operator: Dist): Dist = Dist(thisDouble * operator.metres)

I guess that should just stay the way it was. Unless explicitly specified otherwise.
This SIP is only about how extension methods are defined and where the compiler looks for them.

The equivalent Scala-fied Kotlin would be

    def List[List[T]].flatten [T]: List[T] = 
      this.foldRight(Nil)(_ ++ _)

or

    def [T] List[List[T]].flatten: List[T] = 
      this.foldRight(Nil)(_ ++ _)

I think this obscures what should be expressed on several levels. I am not fond of it at all. You get clearer syntax without needing special rules for this at the price of having to write one extra parameter name. That’s a tradeoff worth making.

2 Likes

I could get used to this. What is the argument before the method name called? It seems that it warrants having its own way of talking about it, since it’s not really a parameter list. In the grammar I can see that you can only have one parameter list containing one parameter, but I can see this difference between it and the “regular” parameter lists confusing new people.

I’d like to see an example of something that’s an extension method, and it also requires some context. I’m guessing here, but something like,

def (c: Circle) area given Context: ...

Would this be legal? Which extension method would be called for the Square?

trait Rectangle {

def a: Long

def b: Long

}

case class GenericRectangle(a: Long, b: Long) extends Rectangle

case class Square(a: Long) extends Rectangle {

def b: Long = a

}

def (rectangle: Rectangle) area: Long = a*b

def (square: Square) area: Long = a*a

val rectangles = List(GenericRectangle(2, 3), Square(5))

val areas = rectangles.map(_.area) // which would be called for the Square here?

just copied curoli code in more readable way :slight_smile:

trait Rectangle {
  def a: Long
  def b: Long
}

case class GenericRectangle(a: Long, b: Long) extends Rectangle
case class Square(a: Long) extends Rectangle {
  def b: Long = a
}

def (rectangle: Rectangle) area: Long = 1
def (square: Square) area: Long = 2
val rectangles = List(GenericRectangle(2, 3), Square(5))
val areas = rectangles.map(_.area) // result: Seq(1,1) compiled using Dotty 0.13.0-RC1

While calculating val areas it’ll use Rectangle implementation because rectangles has inferred type List[Rectangle] (most specific type).

val squares = Seq(Square(1), Square(100))
squares.map(_.area) // Seq(2,2)
(squares:Seq[Rectangle]).map(_.area) // Seq(1,1)

And currently it does not work with implicits:

def (square: Square) something(implicit ec:ExecutionContext): Long = 2
Square(1).something
\\ ERROR:value something is not a member of Square ...
Square(1).something(ExecutionContext.global)
\\ ERROR:value something is not a member of Square ...

def (square: Square) something(ec:ExecutionContext): Long = 2
Square(1).something(ExecutionContext.global)
\\ Now it compiles

And currently it does not work with implicits

There’s a PR to fix this: Strengthen overloading resolution to deal with extension methods by odersky · Pull Request #6116 · lampepfl/dotty · GitHub

In a nutshell overloading rules are as follows:

Given a call x.m(args)

  • all members of x are considered first.
  • if no member matches, all imported extension methods are considered next.
  • if none of these matches either, we search for an implicit value that contains an extension method m. If a unique value is found, we consider its extension methods, which might again have overloaded variants for m.

Overloaded extension methods cannot be obtained from several implicit values. E.g. this code would
give an implicit ambiguity error:

  implied A {
    def (x: Int) |+| (y: Int) = x + y
  }
  implied B {
    def (x: Int) |+| (y: String) = x + y.length
  }
  assert((1 |+| 2) == 3) // error: ambiguous

However, one can always disambiguate in this case by importing one extension method which then takes precedence.