Extensions methods with default arguments

I can compile this, inside the same package:

extension (str: String) def foo(x: Int) = ???
extension (n: Int) def foo(x: Int) = ???

but I cannot compile this (using 3.1):

extension (str: String) def foo(x: Int = 0) = ???
extension (n: Int) def foo(x: Int = 0) = ???

The compiler rejects it with a two or more overloaded variants of method foo have default arguments error. I’m sure there’s a reason for it (i.e., that these extensions are implemented as overloaded methods), but from a user perspective, it makes no sense, and it prevents us from having extensions on separate types have the same name (my current case is the same extension on Iterator and on LazyList).

Also, is there a current workaround, other than defining the extensions in separate objects/packages?

5 Likes

also use site error is weird.

-- Error: ext-defaults.scala:5:8 ----------------------------------------------------------------------------------------------------------------
5 |def f = 42.foo()
  |        ^^^^^^
  |        value foo$default$2 does not take parameters
-- Error: ext-defaults.scala:3:23 ---------------------------------------------------------------------------------------------------------------
3 |extension (n: Int) def foo(x: Int = 0) = ???
  |                       ^
  |                       two or more overloaded variants of method foo have default arguments
2 errors found
2 Likes

As relevant background, note that both Scala 2 and Scala 3 reject the following code:

scala 2.13.7> class C {
            |   def foo(s: String = "") = ???
            |   def foo(i: Int = 0) = ???
            | }
                    ^
              error: in class C, multiple overloaded alternatives of method foo define default arguments.
Welcome to Scala 3.1.0 (1.8.0_292, Java OpenJDK 64-Bit Server VM).
                                                                                                                        
scala> object O:
     |   def foo(s: String = "") = ???
     |   def foo(i: Int = 0) = ???
     | 
-- Error:
3 |  def foo(i: Int = 0) = ???
  |      ^
  |      two or more overloaded variants of method foo have default arguments

(But I’m not expressing an opinion on whether the extension-method version could be addressed separately.)

There’s a quite nice undocumented solution for this https://github.com/lampepfl/dotty/issues/13613.
So basically instead of defining extensions in objects you can define them in givens (their types don’t really matter because we’re not going to summon them so we can use ‘{}’, which is an alias for ‘Object’ so and it won’t spoil the implicit search for more specific types). Whenever a given is in scope all extension methods defined inside it are available to be invoked as extensions but they don’t pollute the current namespace so they’re less likely to clash with each other.
So in your case you could have something like

given stringFoo: {} with
  extension (str: String) def foo(x: Int = 0) = ???

given intFoo: {} with
  extension (n: Int) def foo(x: Int = 0) = ???

def s = "abc".foo()
def i = 123.foo()

Note that the givens have to be named explicitly because otherwise the synthesized names would be the same and that would cause an error.

3 Likes

This is nice. I’m actually impressed that there is not ambiguous implicit issue.