Finally with the release of 3.4.0 I was able to test SIP 54 - Multi-Source Extension Overloads (aka relaxed extension methods) on my library (until very recently was blocked by regressions, so nightly version was not eligible).
It seems that SIP 54 was a good step forward, however not enough IMO.
Consider the following example (scastie link):
class Foo[T]
object Lib:
extension (foo: Foo[Int])
def bar: Unit = {}
import Lib.*
extension (foo: Foo[String])
def bar: Unit = {}
val f = Foo[Int]()
f.bar //error
If a library defines an extension method on one type and I define a different extension method with the same name in my own code on a different type, the overloading mechanism chooses the method according to the classic scoping mechanism instead of choosing the best method for the extended type.
No error is generated and everything works as expected if we use implicit classes (scastie link):
class Foo[T]
object Lib:
implicit class FooIntExt(foo: Foo[Int]):
def bar: Unit = {}
import Lib.*
implicit class FooStringExt(foo: Foo[String]):
def bar: Unit = {}
val f = Foo[Int]()
f.bar //works!
I think we’ve gotten used to implicit class semantics, and this should be preserved.
What happens if we convert the standard library to Scala 3 to use extension methods instead of implicit classes?
What happens if for the upcoming Named Tuples feature a library creates an extension method for its named tuples and its users have their own extension methods on different named tuples, but with the same name?
I think in both cases we get ambiguities or errors that can be a hassle to workaround. SIP 54 already special-cased extension methods for overloading. We need to take this even further and for extension methods apply priorities according to the extended type match, and only then apply the scoping priorities.
class Foo
object Lib:
implicit class FooIntExt(foo: Foo):
def bar(a: Int): Unit = {}
import Lib.*
implicit class FooStringExt(foo: Foo):
def bar(a: String): Unit = {}
val f = Foo()
f.bar(42) //works!
I think instead of spending so much time discussing niche feature like list literals, it’s better to try and find a good solution for the limitations of extension methods.
I’m running into this issue when working with new types in Iron.
There are cases where I want to define methods with the same name, as shown in the code below.
The way you do this is to declare a trait with the operations you want to overload and declare your extension methods in the trait.
trait Semigroup[A]:
extension(a: A) def add(b: A): A
object Stuff:
opaque type Height = Int
object Height:
given Semigroup[Height]:
extension(a: Height) def add(b: Height) = a + b
opaque type Width = Int
object Width:
given Semigroup[Width]:
extension(a: Width) def add(b: Width) = a + b
import Stuff.*
def addAll(l: List[Height]): Height =
l.reduce(_.add(_))
This is the solution to 95% of the issues that people have with extension methods.
Maybe just to add, this still works even if one does not have a common supertrait:
object Stuff:
opaque type Height = Int
object Height:
given syntax: AnyRef with
extension(a: Height) def add(b: Height): Height = a + b
opaque type Width = Int
object Width:
given syntax: {} with
extension(a: Width) def add(b: Width): Width = a + b
import Stuff.*
def addAll(l: List[Height]): Height =
l.reduce(_.add(_))
Though, both ways to write it (AnyRef with or {} with) are kinda weird (suggestions welcome).
The reason is that extension methods are found on objects in the implicit scope of the involved types.
As mberndt says, this solves basically all issues with extension methods conflicting or being in the way.
Naming the givens is also quite useful if you add extensions to something you dont control, because then you can just import my.module.syntax to make the extension methods available, but without polluting your namespace.
If you dont like the nesting, you can also just make the companion object given, though I am not sure if that is specified to work like that or just a compiler artifact:
object Stuff:
opaque type Height = Int
given Height: AnyRef with
extension (a: Height) def add(b: Height): Height = a + b
opaque type Width = Int
given Width: {} with
extension (a: Width) def add(b: Width): Width = a + b
import Stuff.*
def addAll(l: List[Height]): Height =
l.reduce(_.add(_))
Though, this means the companion object is no longer imported by the * import in case you need it for anything else …
@mberndt@ragnar
Thank you for providing the solution.
I had assumed that extension was simply a replacement for implicit class, so it felt inconvenient that I couldn’t just swap an implicit class for an extension and use it the same way.
It seems like this could be troublesome when migrating code from Scala 2 to the Scala 3 style.