Ambiguity when a method signature with a default-value parameter matches another method signature?

Suppose I have two method signatures that nearly match, with one having an optional parameter with default value:

func doSomething(with foo: String) -> Int {
    print("Called the first!")
    return 0
}

func doSomething(with foo: String, andMaybe bar: String? = nil) -> Int {
   print("Called the second!")
   return 1
}

doSomething(with: "abc")  // returns 0, not 1

I understand why the compiler chooses the first, as the first has the actual method name of doSomething(with:) while the second has the method name of doSomething(with:andMaybe:). But this seems like an easy mistake to make.

Am I missing something, or does this seem like something the compiler should warn about?

The compiler ranks the function overloads according to various rules and chooses the most “specific” one. In this case, the compiler chooses doSomething(with:) because it has fewer effective parameters.

Sure, I understand how the compiler chooses the first. I guess what I meant is, without a warning to the user, it would be far too easy for the user to write code which accidentally causes the invocation of the wrong method.

I've seen in Xcode that if you write a method that very closely matches a common delegate method (like tableView:cellForRowAt:), the compiler generates a warning that basically says, "this is almost a delegate method, did you mean to write the delegate method?" It'd be nice if the compiler did something similar here - it would generate a warning that says something like, "there are two methods with nearly identical signatures and we're going to choose this one - did you mean to include the second parameter for the second one?"

Okay, how can a user silence that warning if the user did indeed mean to call the first function? Should they change the signature of the second function?

Maybe? It's hard for me to imagine a case where a developer would have intentionally written two methods with near-identical signatures, where one has a default parameter that causes it to look like the other. But I'll admit I don't have a good imagination for use cases outside of my own world. :slightly_smiling_face:

Full disclosure: I work with Jason

I think this is one of those sharp corners of the language, which is to say I know it is like it is for a good reason, but it's still something easy to overlook that can bite you if you aren't careful.

I will say, I'm glad the language is resilient to code changes, in the sense that adding a default value to that second parameter at a later point in time would not change the semantics of any call sites that had been using the first method. That's certainly why the compiler uses the "most specific" rule to resolve ambiguity.

The one place where this can "bite" you is in the following scenario:
Say you're implementing a protocol or inheriting from a class that defines and implements doSomething(with:). You then write your own method doSomething(with:andMaybe:) but decide to add that default parameter for some good reason. You then go to use your method somewhere, not realizing you're actually calling the inherited form.

This is the exact scenario Jason found himself in and, I think, is why this topic has been brought up a few times before.

A warning would certainly be helpful in this scenario, but it would be obnoxious in others, so finding a reasonable way to silence it is critical. Here's some scenarios and imperfect solutions:

  • If you intended to call the arity-2 function, silencing the warning is trivial because you can simply add the second parameter to your call. The fix-it could even insert the default value for you. It's annoying to have to specify what should be a default, but there's just no other way to do it and it's the right move, even without the warning.
  • Removing the default value would also be a sensible fix-it, but could only apply if you own that code. If you only happen to own the short-form method, there's nothing reasonable you could do except rename it.
  • Other more dramatic changes like changing the parameter or function names could be recommended, but I'm not sure the compiler would want to do that since it wouldn't have any way to generate a recommended name. It's not like the near-miss protocol conformance scenario — there's concrete to compare against. I suppose it could just point you to the func definitions and say "consider renaming".

One way to silence the warning comes to mind which would work in all scenarios. A fix-it could suggest you use the fully-qualified name as an unapplied reference. For example:

Did you mean?
  doSomething(with:)("abc")
  doSomething(with:andMaybe:)("abc")

I admit this is truly strange usage of function calls, but it's perfectly valid and does what you'd expect. Typically you'd only use the unapplied reference when you want to, say, pass one of the functions as a parameter — point-free style.

This is the only way to remove all ambiguity, but considering how strange it is I dunno if this is the right move.

But then again, since this scenario is rare enough in the first place, maybe using a rarely-used feature is appropriate!

It would be nice to highlight this surprising corner-case for people. Even veterans can trip over this ambiguity from time to time.

1 Like

This is a very dangerous move. The API can change its default value, which will not be reflected in the fixed version.

That actually wouldn't work if the function has multiple defaulted values.


Using type inference to decide between two signature is so ingrained on Swift. I'm not sure there's a good solution that uses only existing call syntax and/or without changing the type inference rule.

Do you mean a situation like this?

func doSomething(with foo: String, andMaybe bar: String? = nil, finally: String? = nil) -> Int {
    // ...
}

let x = doSomething(with:andMaybe:finally:)("")

This actually works just fine.
I'm starting to think this might be the "right" way to disambiguate and silence the warning.
I agree with you, hard-coding the default value isn't great for a lot of reasons.

If you needed to disambiguate like this, maybe should not overload in the first place to avoid confusion? If it's all your code that's...

Ok, I realize sometime you overload something without knowing and end up calling the wrong thing...

Sure, no argument that you shouldn’t write ambiguous code. But Swift is a language designed to be safe by default, and this seems like one of those areas where an easy mistake to make can give you head-scratching behavior that’s not obvious to track down immediately.

I agree with you. The compiler shouldn't just pick one arbitrary. It should be a compile error so you either change the function name or explicitly disambiguate.

Too bad Swift willingly let you write incorrect code.