Updated Proposal: Revisiting Implicits

Yes, it still works, but I think you are missing my point. My goal would be to have many different given Converter[A,B] implementations available and very few, carefully chosen Conversion[A,B] implementations. Implicit conversions still have their place and are occasionally necessary. I don’t what a huge plethora of implicit conversions available as soon as someone enables the feature flag. Type safety would go completely out the window. I want developers to feel free to provide Converter[A,B] implementations willy-nilly, without fear that these conversions will start happening implicitly as soon as someone enables the feature flag.

Of course, I can’t prevent someone from shooting themselves in the face by doing this:

given[A,B](given C: Converter[A,B]): Conversion[A,B] = C(_)

But I think that requires considerably more malice of forethought than simply enabling a language feature flag.

Check what I proposed again, it goes the other way:

This way you can access existing Conversion instances through the Convertible interface, but you would not be able to access the Convertible instances via implicit conversions.

It’s a shim for backwards compatibility, not a footgun like (as you pointed out) a lift from Convertible to Conversion would be.

Yes, I know. I meant what I wrote. The lift from Conversion to Converter is safe and sane, and there is no reason not to provide it.

Then can you expand on your previous comment, because I don’t understand this in the context of opposition to a lift from Conversion to Convertible (it only makes sense to me under the assumption of a misunderstanding that a Convertible instance would provide a Conversion):

The statements themselves make sense, and I agree with them, I just don’t see how they fit with a pushback against providing a shim for backwards compatibility. I don’t think it would encourage implementing the instances as Conversion rather than Convertible - if nothing else, having to import the feature flag should act as a barrier.

What you say works. There is nothing wrong with it. I just think it makes more sense to provide a Convertible (or Converter) trait in the standard library and have Conversion extend that.

1 Like

Fair enough, that’d be another way to get there.

And I agree that having this in the standard library would be really, really nice.

1 Like

Several people were having concerns about the dual use of given as a provider and a consumer. In a sense that dual use is inevitable since given parameters are both consumers and providers. But it’s still a concern if given's with different meanings are used next to each other. Here’s a new syntax proposal for conditional given instances that avoids these confusing cases.

4 Likes

I am very happy the concerns were taken seriously. I have to get used a bit to the new proposal, but it seems to make more sense than the previous one. I think, given time, this feel a lot more natural than the current implicits.

1 Like

One more thing about comparisons of Scala to Haskell (assume modifiers here expand to: defs, vals, lazy vals, etc). Haskell doesn’t explicitly differentiate using modifiers when defining typeclass instances because Haskell never explicitly differentiates using modifiers in any case (i.e. the distinction is always implicit). Therefore removal of modifiers in given definitions doesn’t bring Scala closer to Haskell w.r.t. uniformity - actually it’s the opposite.

For example in Haskell name = expression translates always to lazy val name = expression while in Scala we can use def, val or var in addition to lazy val. Scala programmers are taught about the differences between def, val, var and lazy val from the beginning so there’s no increase in difficulty when applying these to given definitions. I argue that actually there’s a decrease as making modifiers explicit removes cognitive load if the rules for making modifiers implicit are non-trivial. In Haskell the rules are trivial (no parameters = lazy val, parameters = def). In Scala 3 they aren’t.

(disclaimer: I don’t have a lot of experience in Haskell. I’ve mainly done learnyouahaskell.com and then a little extra things, so I may be wrong here)

4 Likes

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