Overloaded methods with multiple blocks that some have default values

I wanted to write something like the following:

trait Ctx
object Ctx:
  val default: Ctx = new Ctx {}
def foo(arg: Int)(using ctx: Ctx = Ctx.default): Unit = {}
def foo(arg1: Int, arg2: Int)(using ctx: Ctx = Ctx.default): Unit = {} //error: two or more overloaded variants of method foo have default arguments

I want both foos to have a default values for their contexts, but I’m currently limited since I get the overloading ambiguity error.

Why isn’t overloading ambiguity checked on a block-by-block basis?

1 Like

It’s not ambiguous. You can’t have default parameters for more than one of a set of overloaded methods. It’s one of the ways in which overloading isn’t quite first class. It annoys me greatly, because it makes it considerably harder to write lovely APIs.

But, anyway, it’s not an ambiguity issue. This also doesn’t work:

object Nope:
  def foo(arg: Int)(eel: String = "eel"): Unit = {}
  def foo(arg1: Int, arg2: Int)(eel: String = "eel"): Unit = {}

And this does work:

object Yep:
  def foo(arg: Int)(eel: String): Unit = {}
  def foo(arg1: Int, arg2: Int)(eel: String): Unit = {}
  def bar(arg: Int)(using Boolean): Unit = {}
  def bar(arg1: Int, arg2: Int)(using Boolean): Unit = {}

Overloading ambiguity resolution is also less powerful than it could be, but in this case it’s the multiple default parameters which is the problem.

Incidentally, with named tuples there is a partial workaround, but you lose the ability to name your parameters in opposite order:

object Ugh:
   type TwoArgs = (arg1: Int, arg2: Int)
   type UghArg = Int | TwoArgs
   inline def foo[A <: UghArg](a: A)(using bool: Boolean = false): Unit =
     inline a match
       case arg: Int => println(s"$arg $bool")
       case args: TwoArgs => println(s"${args.arg1} ${args.arg2} $bool")
       case _ => compiletime.error("Indeterminate argument type")

And then:

scala> Ugh.foo(3)
3 false
                                                                                
scala> Ugh.foo(arg1 = 5, arg2 = 7)
5 7 false
                                                                                
scala> Ugh.foo(9, 9)
9 9 false
                                                                                
scala> if true then
     |   given Boolean = true
     |   Ugh.foo(3)
     |   Ugh.foo(9, 5)
     | 
3 true
9 5 true

But if you try to invert the order:

scala> Ugh.foo(arg2 = 1, arg1 = 2)
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |Ugh.foo(arg2 = 1, arg1 = 2)
  |        ^^^^^^^^^^^^^^^^^^
  |        Found:    (arg2 : Int, arg1 : Int)
  |        Required: Ugh.UghArg
  |
  | longer explanation available when compiling with `-explain`
1 error found

The limitation is there to prevent possible ambiguity at the call site. But clearly like in the case above, it’s not possible to create such ambiguity at the call site, so I’m wondering if there is a way to fine-tune the rules to allow for default parameters in methods that are clearly distinguishable by earlier blocks.

I don’t think the limitation was only to prevent ambiguity. It also meant that one didn’t have to worry about how to (stably) name the different defaults.

But, anyway, I would be happy if more non-ambiguous cases were allowed!

The limitation is entirely an implementation restriction. It has to do with how default parameters get compiled down to the JVM/JS/Native model. The type checker would be happy to deal with multiple overloads with defaults. It’s the rest of the compilation pipeline that cannot cope.

2 Likes

Proposal to change that: Better Default Arguments

4 Likes