Updated Proposal: Revisiting Implicits

There are some new developments on the syntax of extension methods in
PRs #7914 and #7917.

Direct link to doc page

Comments are welcome, either here or directly on these pull requests.

5 Likes

Hi Martin,

My main comment is that there seems to be a few different ways of defining extension methods. Do we need them all?

For someone reading Scala code, I would expect that there is a newbie cognitive cost to having several different approaches of effectively defining the same thing. Hence, I wonder whether it would be worth taking a critical look and check that all of the approaches are required, and perhaps whether any could be culled?

E.g. Perhaps we could drop anonymous collective extensions?

I’m also not sure that I really like have the object being extended to the left of the method name. I can see that it is quite cute in a way, but at the same time, is having an additional syntax for defining methods really helpful to readers?

Anyway, I hope the feedback is useful.

Regards,
Rob

My first reaction was also that two ways to define extension methods was too much. The comment in #7917 explains why I changed my mind and now believe we need both regular extension methods and collective extensions. But it’s worth considering dropping the anonymous variant.

3 Likes

Offering an alternative convenient syntax for singleton extension methods, strikes me as consistent with the way SAM syntax is convenient for single abstract methods.

I like the collective extension syntax. Much better now, IMO.

@odersky I think that single line extension methods should also have the extension keyword to make it clear that this is an extension def.
Instead of
def (c: Circle) circumference: Double
We get
extension def (c: Circle) circumference: Double

I think with this extension def the syntax is much clearer (with or without the extra . dot).

5 Likes

I don’t feel that extensions are sufficiently unified with conversions, which can do exactly the same thing according to the docs. Either conversions shouldn’t allow you to call methods (i.e. a method call would not be a request to convert the type), or the unification should be clearer. In particular, all the extensions should be instances of Conversion.

I don’t like that def (foo: Foo)bippy is the same as def bippy(foo: Foo) but def bippy(foo: Foo) doesn’t create an extension method. If it’s the same, it should be bijectively the same. If not, make it different.

I also don’t like that the syntax is looking so much like C function pointers.

Also, the AnyRef thing on givens feels like a hack.

If it is important to be able to specify whether a leading parameter can be supplied on the left before a dot, I would instead use this to denote that:

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

circumference(myCircle)   // Fine because it's just a def
myCircle.circumference    // Fine because you said this was cool too

Alternatively, just allow any leading parameter to be called, in any context!

import scala.math._

0.75.tan   // Same as tan(0.75)

If you need to get a bunch of them in scope at once, stick the methods in an object and import the contents. If you don’t want to repeatedly name the parameter, just use a conversion. It already does the job. Yes, it’s got a bit more boilerplate. If that’s too much, I’d suggest that the problem is with givens and conversions, not with collective extensions.

(Alternatively, as I said, don’t let conversions happen automatically on method calls. If you want extension methods, use extension methods, not conversions.)

We have been over the syntax of conventional extension methods many times in and in depth, and have experimented with a large set of variants. I think we are pretty much settled on the current design.

We never formally discussed the variant that every method can be an extension method (AFAIK that’s the rule in D). I have given it some thought previously and concluded that it would lead to precisely the same overreach of freedom of expression that we suffer with infix methods. Some people will write 0.75.tan and others tan(0.75). We worked hard to get away from that with @infix, so I think it’s a bad idea to open a second can of worms at the same time as we are closing the first.

2 Likes

Okay, but possibly that should mean that you should not be allowed to invoke an extension method as “circumference(myCircle)”, perhaps the only valid invocation of an extension method should be of the form myCircle.circumference".

One other comment on the documentation. Whatever you decide to end up with, I think that it could be helpful to document how it works if the extension method takes additional parameters. I.e. I’m not sure it is obvious to me whether:

def (c: Circle).methodName(arg: String): Double would equate to:

def methodName(c: Circle)(arg: String): Double, or

def methodName(c: Circle, arg: String): Double

Or perhaps even:

extension (c: Circle) def circumference: Double

But thinking about this a bit more, I wonder how many extension methods really will be defined. Perhaps the singleton case syntax could be dropped and just the collective syntax retained?

Personally, I think that the less ways there are to write something in a language the simpler it becomes to read.

I like the extension syntax and I think it is a good idea to separate it from the given concept.

However:

In given stringOps: AnyRef it looks like we are defining a given instance of AnyRef. This is confusing. Why would we do that? What is the sigificance of AnyRef here? It looks very low-level and out of place

Some people will write 0.75.pipe(tan), but it’s a fair point.

(On the other hand, there’s a quite a bit of boilerplate extending e.g. Double to include scala.math operations as postfix operators, e.g. 2.7.abs works, and people do use that sometimes instead of abs(2.7), so I think the ship has already sailed…but maybe we want to restrict the size of the fleet.)

Anyway, you can’t call def bippy(a: Foo)(b: Bar) as bippy(foo, bar) only bippy(foo)(bar), even though in the bytecode they’re the same thing, so if the extension syntax is fixed I recommend disallowing def (a: Foo)bippy(b: Bar) to be called as bippy(foo)(bar). Having to remember when a.quux(b) is the same as quux(a)(b) and when it’s different does not sound like fun to me.

2 Likes

In given stringOps: AnyRef it looks like we are defining a given instance of AnyRef . This is confusing. Why would we do that? What is the sigificance of AnyRef here? It looks very low-level and out of place

That’s precisely why we have extension syntax. No need to make the given equivalents nice.

1 Like

Yes, that’s a possibility.

2 Likes

I do think we should indeed forbid to call extension methods as if they were normal methods.

The main argument (the only one?) for allowing that was that it provided for a very simple explanation of the semantics of extension methods: they are simply desugared into the normal method equivalent. We can still preserve this simple explanation; we just have to say that both the definition and the use sites have the same semantics as if they were desugared into the normal method equivalent (assuming typechecking has already agreed that the program was correct).

5 Likes

Probably looks pretty crazy, but how about something along these lines to unify the 2 ways to expres extension methids?

  given Ctx[A]
    def (theA : A)
        f (otherA : A) = ???
        g (otherA : A) = ???
        [B] h (theB : B) = ???
    def (nested: Ctx[Ctx[A]]) i (otherA : A) = ???

or

  given Ctx[A]
    extend (theA : A) with
      def f (otherA : A) = ???
      def g (otherA : A) = ???
      def [B] h(theB : B) = ???
    extend (nested: Ctx[Ctx[A]]) with 
      def i (otherA : A) = ???

n-th edit: OK, the second one actually doesn’t even look crazy. A bit wordy if you want to have extension methods for a lot of “receivers” of different types but like it a lot

One other reason for allowing this is that it provides a helpful tool for debugging why an extension method doesn’t resolve. The example below is quite trivial, but the difference between the content of the two error messages can make a world of difference when typeclasses or generalized type constraints are involved.

def (a: String) bracket: String = s"[$a]"
  
println("hello world".bracket)
println(bracket("hello world"))

// Error: value bracket is not a member of Int
// println(5.bracket)

// Found:    Int(5)
// Required: String  
// println(bracket(5))

scastie

Clarification: I do agree this is something that probably shouldn’t be in the code when it’s done, so a linter rule would be entirely appropriate.

1 Like

There are some situations where the only way to call an extension method is as a normal method (example in #7821).

3 Likes

@nicolasstucki - Then we shouldn’t have those.

For instance

a org.com.edu.Ops.+ b

Yes, it’s absurdly ugly, but it’s also absurdly regular. Or

import org.com.edu.Ops.{ + => ops_+ }
a ops_+ b

I really think we need to step back and consider the overall regularity and simplicity of the resulting language.

Whenever we end up with “well, X isn’t so good, but because Y…”, we should think hard about whether Y is really worth it, and whether Y can’t be altered so that not-so-good-X is avoidable.

3 Likes

Let’s fix the compile-time messages, not allow multiple ways to call things and then force the user to switch between them in order to get useful messages.

If anything, this is even more of an argument to forbid going both ways: during debugging, people will randomly switch from postfix, as intended, to function-with-arguments style, and then leave it after it compiles.

3 Likes

That’s a nice goal, but until we get there, we should probably avoid crippling our ability to debug extensions when things go wrong :slight_smile:

Linters can help us remember to switch things back, until the error messages become clear enough to make this facility redundant