Proposal to add Extension Methods to the language

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.

In all Smalltalk based languages, this or self is always an implicit constant parameter.

Method calls are always pattern match between ā€œthisā€ and function tables. All OO languages I know of (including non-Smalltalk based languages), the instance has a pointer to the class. Which means that the function lookup naturally has a small set of methods to match.

The first OO languages (I started with McCarthyā€™s lisp), didnā€™t have implicit parameters. In CommonLisp and Clojure they used a macro called ā€œdefmethodā€, but there arenā€™t any methods. Everything is a function. Specialized functions do pattern matching based on their parameters to select the proper implementation to evaluate. Like the Hotspot compiler, the dispatch lookup is usually optimized for hot dispatches.

From that perspective everything is
f(oref,ā€¦)
Which is matched to an implemented method, then evaluated. In scala syntax thatā€™s
oref.f(ā€¦)
Within the body, scala has the val ā€œthisā€ that is the local scope name for oref. The compiler looks to method instance for local execution variables, if there are no collisions, calls or object instance variables.

1 Like