2 min read

Opaque vs Existential vs Concrete Type Hidden Ambiguity in Swift

Related: iOS App Development, Software Engineering


Came across an interesting behavior in Swift which I found not only curious but also not obvious and VERY error-prone.

This is related to how Swift would prioritise implementation when dispatching function calls.

Tested on Xcode 15.3 Swift 5.10

Some vs Any

  • some indicates an opaque type which known to the compiler at compile time. Type that conforms to a protocol
  • any indicates an existential type – a box whose contained value has a type which is not known to the compiler at compile time. The contained value type conforms to a protocol

Example 1. Some vs Any

Here is a simple example:


func foo(_ param: any Equatable) {
	print("I'm any equatable")
}

func foo(_ param: some Equatable) {
    print("I'm some equatable")
}

if we call

foo(true)

The result would be

I'm any equatable

Example 2. Generic Type vs Any

The implementation with some keyword is identical to an old school style generic implementation:

func foo<T: Equatable>(_ param: T) {
    print("I'm generic<T> equatable")
}


func foo(_ param: any Equatable) {
	print("I'm any equatable")
}

If we run it again, we will see the same result – Swift will ignore the implementation with the compile-time type check:

foo(true)

The result would be:

I'm any equatable

Example 3. Some vs Any vs Concrete Type

However, things would be different If we provide a concrete type implementation:


func foo(_ param: any Equatable) {
    print("I'm any equatable")
}

func foo(_ param: some Equatable) {
    print("I'm some equatable")
}

func foo(_ param: Bool) {
    print("I'm bool")
}

Run:

foo(true)

The result would be :

I'm bool

What's Bad in This Design

First of all, it's easy to confuse. The mnemonic rule could be to think of a foo(:Bool) as if it is overriding foo(:Equatable) because Bool conforms to Equatable.

I would love to hear your ideas on some vs any mnemonic rules here.

Secondly, it's controversial.

  • In the case of some vs any protocols, the compiler disregards the implementation with the type known at compile time and picks the implementation with a runtime check.
  • In the case of a concrete type it prefers the implementation with a type, known at compile time.

Thirdly, it's error-prone. The compiler doesn't even produce any warnings for clearly ambiguous some vs any implementations. It's fairly easy to accidentally override something in an extension without even knowing about it.