Default Overload Annotation for Ambiguous Calls

In cases where one overload is used 99% of the time, there should be an annotation to mark it as the default. A perfect example is String.contains, which now often requires String literals to be explicitly cast to avoid ambiguity with the Character and other StringProtocol overloads:

desc.contains("simple" as String)  // Why is this necessary?

The new annotation could work like this:

extension String {
    @defaultOverload
    func contains(_ other: some StringProtocol) -> Bool { ... }
    
    func contains(_ element: Character) -> Bool { ... }
}

Usage

desc.contains("a")              // Uses String version (default)
desc.contains("a" as Character) // Explicit Character version

The compiler would select the annotated overload when the call is otherwise ambiguous. This adds no loss of safety — just better ergonomics for the common case.

Without getting into your actual proposal, I want to note that your example works today without any disambiguation:

desc.contains("a") // calls the Character version
desc.contains("simple") // calls the String version

The rules that make that true are certainly a bit opaque and I don't think they're written down in one place, but you might want to find a different motivating example.

2 Likes

This is news to me.

Are you saying that, with two viable overloads available for desc.contains("a"), Swift is not choosing the one which uses the default literal type?

Edit: oh wait, I just re-read the example declarations. It’s choosing the one with a concrete type. Nevermind.

1 Like

No, not using those, using the real stdlib. String literals have always preferred the most specific ExpressibleBy* protocol, though it’s just one of multiple ranking criteria and I’m pretty darn sure I can contrive examples that wouldn’t have that behavior because other ranking rules are stronger. This just isn’t one of them.

Hello jrose - You're right. Those are bad examples because they lack context. They do, however require specifying the String type. I'll explain it with better examples and context:

The issue is overload resolution ambiguity when string literals can satisfy multiple type constraints. Here's a detailed explanation with examples:

Swift string literals conform to ExpressibleByStringLiteral, which means they can be implicitly converted to any type that conforms to this protocol - including String, Character (for single-character literals), Substring, and custom types.

The problem arises when:

  1. A method has overloads accepting different types that all conform to ExpressibleByStringLiteral or related protocols
  2. The compiler cannot determine which overload to use based on context alone
  3. Unlike earlier Swift versions, the compiler no longer defaults to String when ambiguous

This forces you to add explicit type annotations (e.g. as String) in contexts where the intent is obvious to humans but not to the compiler.

Example 1: String.contains - Collection vs StringProtocol Collision

let description = "Hello, Swift world!"

// ❌ Error: Ambiguous use of 'contains'
// Could be: contains(_ other: StringProtocol) - substring search
// Or:       contains(_ element: Character) - Collection.contains
description.contains("Swift")

// âś… Workaround: Explicit type annotation
description.contains("Swift" as String)

// âś… Alternative: Use range(of:) which is unambiguous
description.range(of: "Swift") != nil

String is both a Collection<Character> and conforms to StringProtocol. The contains method exists on both protocols with different semantics.

Example 2: Generic Functions with ExpressibleByStringLiteral Constraints

func process<T: ExpressibleByStringLiteral>(_ value: T) { 
    print("Generic: \(value)") 
}

func process(_ value: String) { 
    print("String: \(value)") 
}

// ❌ Error: Ambiguous use of 'process'
process("hello")

// âś… Workaround
process("hello" as String)
let str: String = "hello"
process(str)

Again, neither of those examples produce an ambiguity error (you can try them out at https://swift.godbolt.org, from nightly all the way back to Swift 3.1.1).

This line is very suspect:

Unlike earlier Swift versions, the compiler no longer defaults to String when ambiguous

This kind of change would almost always be considered a regression. I say "almost always" because sometimes a breaking change is considered worth it…but I don't remember hearing about it before this post. If you have an example of such a case, please share it if you can, but check that your reduced example still reproduces the problem!


Hello jrose - You're right. Those are bad examples because they lack context. They do, however require specifying the String type. I'll explain it with better examples and context:

These sentences are also suspect, in a different way: it sounds like a stereotypical LLM. Now, it's possible that's coincidence, especially if you were, say, translating to a language you felt more comfortable in and back. But if you just copied my post into your pet text engine and copied back the response as if it were your own words, that's very disrespectful of my time—and I'm inclined to believe that's the case, because the second sentence is incorrect, as demonstrated.

A human took the time to write you a response and help with your idea; you, as a human, should respect that by taking the time to understand what they wrote and replying with your own words.

4 Likes

Jordan - I wrote those, and I certainly didn't mean to disrespect you. I absolutely appreciate your time.

1 Like

I found the issue. A library I imported had a String extension that created the ambiguity. I incorrectly interpreted it as built-in behavior. Thank you for your responses.

1 Like

Yeah…

This is actually a very important point, and I think it needs to be more widely appreciated: introducing a new overload of an existing name is (very often, but always when you don’t expect it) a source-breaking change.

This applies to the case of a library introducing new overloads of its own names in a subsequent release, and doubly so when a library adds members to an extension of some other library’s type.

In theory, it is certainly possible to introduce overloads in a way that doesn’t change the meaning of any code that uses the old overloads, but it’s difficult, given that the disambiguation rules are very subtle, and mostly undocumented.

However, there is a good “rule of thumb” that solves this problem: it is better to think of a set of overloaded declarations that share a common name as an indivisible unit—they should be “born together” and never grow after being published in a source-compatible context.

7 Likes