Two proposed changes for extension methods

That’s correct. @targetName only protects you from double definitions that would otherwise happen if two methods have the same names and erased types. There are still two different showInt methods, and neither is more specific than the other; hence the overloading ambiguity.

One question about implicit class: is it really destined to be deprecated then removed or is the decision not taken yet?

I think it will be removed but it will take several years until we get there.

3 Likes

I feel like there’s something lacking in the way extension methods are currently translated to normal methods:

This:

extension (x: Int)
   infix def min (y: Int): Int = ...

translates into this:

<extension> infix def min(x: Int)(y: Int): Int = ...

I feel like a better translation scheme would be:

<extension> infix def min(x: Int, y: Int): Int = ...

Then, instead of:

def min(lst: List[Int]): Int = lst.foldLeft(0)((a, b) => min(a)(b))

one can write, more concisely:

def min(lst: List[Int]): Int = lst.foldLeft(0)(min)

This falls better with Scala’s current arguments system since arguments in Scala are not curried like Haskell’s.

In many places in Scala, binary instead of curried arguments are used. Translating extension methods into binary-like methods would make them more usable.

Maybe I am wrong about this but at least it deserves a look.

Also I don’t know if it’s be a good idea with multiple type-params extension methods.

1 Like

If would expect to write it like this.

def min(lst: List[Int]): Int = lst.foldLeft(0)(_ min _)

Calling extension methods like regular methods should be the exception anyway IMHO.

3 Likes

I am not saying that:

def min(lst: List[Int]): Int = lst.foldLeft(0)(min)

is better than:

def min(lst: List[Int]): Int = lst.foldLeft(0)(_ min _)

I am trying to point out that, in Scala:

<extension> infix def min(x: Int, y: Int): Int = ...

is more useful than:

<extension> infix def min(x: Int)(y: Int): Int = ...

Can you clarify why you find the binary version to be more useful?

One of the really nice parts of the current translation scheme is that the translation has the same number of parameter lists as it’s source, which is helpful for maintaining a mental model of how it should be called manually (in those few cases where this is needed).

3 Likes

I don’t agree. The practicality entirely depends on the use case. Plus the multiple parameter lists better mirror the definition of the extension method.

And like I said I don’t think this feature should be optimized for calling the “regular” method. I’m not even sure I agree that it should be at all possible to call it like a regular method.

1 Like

Complete aside, but I’ve found this to be really handy when an extension method I’m expecting to compile, doesn’t. Calling it like a regular method usually ends up providing the compiler the extra hint it needs to give me the error message I actually need.

That being said, I can’t think of a single case where I’ve ever committed code with an extension method called as a normal method, so it’s definitely not a normal thing.

Yes agreed, as far as I’m aware that’s the only good reason to call it like a regular method, in absence of a better way to diagnose errors.

1 Like

You need it to call a super implementation, see e.g. https://github.com/rjolly/scas/blob/master/scas/src/scas/structure/commutative/Field.scala

1 Like

@Jasper-M: I do agree that the translation shouldn’t exist. I’d be ok with that.

But if the compiler is going to generate a synthetic method, I’d rather it be:

<extension> infix def min(x: Int, y: Int): Int = ...

and not:

<extension> infix def min(x: Int)(y: Int): Int = ...

This is how min is defined in the standard library. It is how people define methods in Scala. No library or code base uses the above encoding when defining methods. It is unnatural in Scala.

But maybe it’s just my personal preference.

Having independently bumped into the “Ambiguous overload” case @etorreborre shows several times myself (before thinking “surely other people are hitting this too” and going looking) I can attest it’s really quite annoying. Do we have a solution now… or on the horizon?

The problem is not uncommon:

  1. You define some trait-typeclass that includes unary operations on the operand; negate or toJson for example.
def negate(a: A): A
  1. You lift these operations onto the operand itself using extension methods, and typically the extensions should have the same name as the typeclass, since they are indeed the same operation.
extension (a: A)
  def negate: A = negate(a)

But that is disallowed. The extension and the typeclass method have the same signature, error is still as Eric’s example above in Scala 3.1.

I guess (for greenfields cases) we could adopt some conventional prefix/suffix for the typeclass, or the extension, to disambiguate, but it’s not so nice when the operation literally has same meaning in both contexts. I would prefer if the compiler could mangle the extension name to avoid a collision with the related typeclass-trait.

Presently, the extension namespace overlaps & can collide with the surrounding namespace where the extension is defined, in a non-obvious way. This namespace clash doesn’t occur with Scala 2.x implicit wrappers. So extension methods are not currently able to offer a 1:1 replacement for wrappers, which IIUC was one of their design goals.

1 Like

Any reason you’re not making the extension itself abstract, instead of adding a def negate(a: A): A?

1 Like

If I understand correctly, you’re asking why the negate extension forwards on to an abstract method negate(a) on the typeclass, rather than being directly implemented in the extension?

This is the convention in eg the Std lib & Cats of defining typeclass methods in a trait, and then (in Scala 2.x) using implicit wrappers to forward postfix invocations to them. That it’s a established convention is reason enough for me, but I also think the convention is well motivated:

  • Typeclass laws and symmetries are often harder to express and understand when written in the OO, postfix style.
  • Typeclass operations like Monoid.Empty that are arity-0 cannot be invoked on their first operand, and thus cannot be written as an extension method.

The reason your negate extension conflicts with the method is that the extension can also already be called like a method: (instance: MyTrait[Int]) => instance.negate(42) (Scastie - An interactive playground for Scala.)

Who said these should be made extensions? They were not in implicit wrapper classes in Scala 2, and they have no reason to be Scala 3 extensions either.

I try to follow the convention of always adding a capital T to the end of the method name in the type class, as these are not the same methods as the ones in the extension methods and have an extra parameter.

def negateT(obj: T): T //In the type class
def negate: T //in the extension class
2 Likes

A convention like this on the typeclass methods is a sensible response to the present compiler behavior. Exempting presumably, arity-0 methods like Monoid.Empty that are intended to be invoked directly on the typeclass…?

I’ll observe that this convention is a form of name mangling, but done manually by the programmer. IMO it ought to be done by the compiler to automatically disambiguate extensions from the namespaces in which they’re embedded.

1 Like

So to be clear, you want the compiler to bend over backward to support an old Scala 2 convention, although the current behavior is more convenient for Scala 3? In Scala 3, there is no need to duplicate extensions and type class methods. The justifications you gave for the old convention do not hold in Scala 3.

2 Likes