Proposal to add Extension Methods to the language

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.

While I agree that parsing might be problematic (high priority concern), I confess at the same time that I found myself actually wanting to be able to define this kind of DSL-syntax more than once in the past.

I want that kind of DSL too, but I think the cleaner way to get it is probably to introduce ephemeral types. An ephemeral type cannot be stored or discarded; the only thing you are allowed to do with it is immediately call a method on it. Then you could build things up a piece at a time using more or less normal parsing rules.

(In a sense, every mutable operation could be viewed as changing an ephemeral type, but here we make explicit the type and the operation converting it to another type.)

2 Likes

This reminds me a bit of similar Smalltalk syntax which I loved. Why limit it to extension methods? I would prefer that we were able to write all methods in this syntax as it leads to far more comprehension of coder readers, at least for the left-to-right natural languages. Right-to-left languages could simply reverse the argument order, if desired.

2 Likes

In effect I do this now, as I have case classes and opaque types, and everything else is put in with extension methods.

This works due to Translation of Calls to Extension Methods. But new rules seem be ad-hoc and lacking some groundwork. While encoding of type classes looks great we are not restricted to use the feature only this way. In general I can extend the scope with a bunch of functions using implied parameters with extension methods. I can imagine someone will definitely find it useful while implementing DSL or just trying to cut some boilerplate.

trait Connection {
  def (st: Statement) execute: Unit
}
def withConnection(f: given Connection => Unit) = f given new Connection {
  def (st: Statement) execute = println(s"Executed $st")
}

withConnection {
  "SELECT 1".execute
}

Why not go further and introduce scope manipulation as a separate feature so that all other use cases would be covered? For the reference the thread where this feature was discussed.

I guess what is at-hoc is in the eye of the beholder. I find scope injection the way you describe it pretty abhorrent. It’s a hack that brings back the worst memories of dynamic scoping.

The code snippet above demonstrates how scope injection can be achieved with extension methods (the current implementation). It doesn’t look like a feature abuse or a hack but rather a direct usage of the feature. Extension methods are special in a sense that they are visible in the scope of implied object they belong to. But what makes them special? Even though they allow syntactically different invocation mechanism in essence they are just methods, members of an object. I guess they were made special because of the need for a nicer typeclass encoding. But there is no direct support for typeclasses in the language, typeclasses are plain traits. In the end of the day we two types of methods with different scoping rules. That’s why it seems to me that the proposed solution is incomplete. There are a two directions we may take to be consistent:

  1. Special scoping rules for typeclasses. This requires direct typeclass support in the laguage.
  2. Scope management support

I realize I might be a bit late to the party, but I was looking at the syntax change for this and was wondering, if this alternative has been proposed:

For a given class case class Circle(x: Double, y: Double, radius: Double)

you could define an extension method like so:

def circumference(circle: Circle): Double

I’ve seen the alternative def circumference(this circle: Circle): Double, but why do we need to add this? Why not just allow any function that has a first parameter list of only one param, to be available as an extension method to that type? And to be able to be called as:

val c = Circle(0,0,3)
circumference(c) //or
c.circumference()

Seems that this would preserve the familiar scala syntax of defining methods, while allowing for the existence of extension methods.

7 Likes

We have not considered this one yet. It is an interesting proposition. At first glance, it looks technically feasible to make every unary method a potential extension method. The question is whether we want to offer that choice. We are just pedalling back wrt infix methods, in order to remove unncessary choice between two styles of expression. This would add yet another way to express a method call. Users would have to choose between f(x) and x.f. It’s very likely that not everyone would agree on this, so different code styles would proliferate and clash.

11 Likes

I think this is a great approach to extension methods, and it would ultimately simplify Scala.

There would be 2 ways to do it.

More focused one (only for case where the first parameter block has one member):

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

Broader one:

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

Currently there is a tension between function application and method selection and call. This would basically make the distinction almost meaningless.

Also, currently function application is not very practical (annoying, actually) in contrast to method selection and call, because it requires nesting parentheses:

f(g(h(x))) // nested parentheses, so ugly and annoying to write

is not as elegant as

x.h.g.f

There is this problem of applying function to an argument, but without any “nesting” in the syntax, like ( and ).
Other languages deal with this in their own way.

F# (and many others) has |> operator

x |> h |> g |> f

Haskell has $

f . g . h $ x
-- or
f $ g $ h $ x

There is a language called Koka which does something like was proposed above

a.f(b, c) ~~> f(a, b, c)

I see 2 benefits in adopting this extension methods approach:

  1. simplifying syntax (the necessity of nesting () is a wart IMHO)
  2. decrease the difference between thinking of and using functions vs methods

If Scala adopted this, it would even more unify functions and methods, which would be a solution true to Scala’s mission to marry functional and object-oriented programming.

3 Likes

Bravo! Isn’t this conceptually similar to CLOS methods? I.e., it allows me to define method of my generic function (although the generic function doesn’t exist as an explicit object) on classes that I don’t own, and don’t have the source code for? Although as far as I understand, the proposal does not in any way enforce consistency among all the methods of a given name? I.e., two different applications might define method Foo with completely different semantics, and if they happen both be applicable at the same call-site, they would interfere. Right?

Scala 2.13 has “pipe” (which is the same as |> in F#, but implemented in the library), and there’s always function composition as well, but function composition is not the Scala style, and it’s generally a poor fit (in Scala).

I wasn’t going to add my 2 cents in this thread, but since I’ve decided to reply, I think extension methods as described by Odersky is a great idea, and I don’t see space for further tweaking.

Addressing function application is best left to an entirely separate proposal.

1 Like

“pipe”

  1. behind an import.
  2. infix “operator” with an alphanumerical name – this goes against Scala 3 principles.

For these reasons I don’t consider pipe a (practical) solution.

function composition

sadly, as you say, it very very works poorly in Scala :frowning:

extension methods as described by @odersky is a great idea

I agree. But adopting this would be even better! Simpler and more orthogonal, and tackling more that one pain point. Scala is about fusing FP and OOP, this would be a great step towards unifying functions and methods.

. as proposed also an infix operator with a symbolic name. If that’s an argument against pipe, it’s also an argument against .

(I’m not 100% sure you’re responding to me and that I understand your argument)

If that’s an argument against pipe, it’s also an argument against .

It’s not, because . has symbolic name, but pipe does not (it has alphanumerical name, as I’ve written above).

Oh, sorry, my bad.

You could trivially make a symbolic alias though.

In order for an unary method to be used as an extension method It could be annotated with @extension in the same manner as methods are annotated with @infix. I’m not sure if this is any better than extension keyword though.

@extension
def circumference(circle: Circle): Double
2 Likes

Yeah that could be interesting. Then we would just have one way of declaring methods (the usual way, instead of the special extension method syntax), and then a few different ways to modify the syntax with which the method can be called, with annotations like @infix or @extension.

This seems more orthogonal to me than the current situation.

This would also nicely restrict the proliferation of too many extension methods that Martin pointed out would cause a clash of styles, and would set the non-extension usage, as in f(x) not x.f to be the default. Thus extension methods would, once again, be a deliberate choice.

If f(o) is just syntax sugar for o.f(), then what do all the following mean? this.f(o), f(this, o), f(this), this.f(), and f(c).

When first learning Scala one thing that was really confusing to me was the difference between f(o), and o.f(). As I understand it now, if f names a method, then f(o), is just shorthand forthis.f(o)`. Correct me if I’m wrong.