Generics containers and type variance

Hello everyone, I post here because I feel this has to do with some compiler magic, but feel free to move this thread as appropriated!

This is a question that is more for curiosity than anything else.

In Swift, instantiated generics types are all different from one another:

class Animal { }
class Cat: Animal { }
class Dog: Animal { }

struct Bag<T> { }

var b1 = Bag<Animal>()
var b2 = Bag<Cat>()
var b3 = Bag<Dog>()

// None of these is valid: cannot assign value of type
// 'Bag<type>' to type 'Bag<other_type>'
//
// b1 = b2
// b1 = b3
// b2 = b1
// b2 = b3
// b3 = b1
// b3 = b2

Exceptions to this rule exist, in particular container types from the Standard Library are variant:

var a: [Animal] = []
a = [Cat(), Cat()]
a = [Dog(), Dog()]
a = [Cat(), Dog()]

// This is inferred to be of type `[Animal]`
[Cat(), Dog()]

This is true even for containers with multiple generic parameters:

var a: [Int: Animal] = [:]
a[1] = Cat()
a[2] = Dog()
a[3] = Cat()

// This is inferred to be of type `[Int: Animal]`
[1: Cat(), 2: Dog()]

This is true even in this scenario:

// This is inferred to be of type `[[Any]]`
[[1, 2], ["1", "2"]]

// This is inferred to be of type `[[Int: Any]]`
[[1: 1, 2: 2], [1: "1", 2: "2"]]

And I understand all of this, because Any is the supertype of all types. However, how is this implemented?

// This is inferred to be of type `[[AnyHashable : Any]]`
[[1:1, 2:2], ["1":"1", "2":"2"]]

How can the compiler infer or decide that AnyHashable is the supertype of Int and String? How much magic is going on here?

cc @Douglas_Gregor

I cannot answer this question from the view perspective of the compiler, but from the daily Swift user view perspective. As far a I'm aware AnyHashable is a struct with some compiler support, which it would be unnecessary if we had 'opened existentials', but that won't happen anytime soon I guess. If you think of AnyHashable as typealias AnyHashable = /* opened */ Hashable where Hashable also conforms to itself (I think this requirement would be needed for generics - similar to the debate where Error needed to conform to itself) then I would guess that the type resolution for Key in your example would walk some kind of a tree-graph of related types until it finds the first common ancestor type that also satisfies the generic constraint Key: Hashable.

    Any // Cannot be reached for `Key` resolution
     |
  Hashable // AnyHashable when opened 
  /      \
Int    String

It cannot walk up to Any as the constraint then cannot be satisfied which likely would produce a compile-time error. Just create a type that does not conform to Hashable and use it as the key.

struct S {}
[[1:1, 2:2], [S():"1", S():"2"]] // error: Type of expression is ambiguous without more context

The following example resolves to [[String: UIView]]:

[["1": UIScrollView()], ["1": UIWindow()]]
1 Like

So. Much. Magic.

We have a special sub typing rule that allows any Hashable type to be a subtype of AnyHashable.

We also have a rule that, for the key type of a dictionary literal, if you can't find a common type among the elements, try AnyHashable. There's a similar rule for array literals to try Any if you can't find a common type among the elements.

Doug

4 Likes

Ah okay, there are special rules baked in. I guess it could not be done in other ways, without introducing new "stuff" to Swift, right?

AnyHashable has a computed property (base) of Any type, and its documentation says to use base for casting back to a concrete type.

let anyMessage = AnyHashable("Hello world!")
if let unwrappedMessage = anyMessage.base as? String {
    print(unwrappedMessage)
}

But because AnyHashable has special subtyping rules as @Douglas_Gregor explained, the following (without .base) works too:

let anyMessage = AnyHashable("Hello world!")
if let unwrappedMessage = anyMessage as? String {
    print(unwrappedMessage)
}

Is there a reason why base is a public API? It's a bit inconsistent with other existential containers like AnySequence, which does not expose a base property. Source compatibility aside, would it ever make sense to remove base, or to discourage the use of it?

1 Like
Terms of Service

Privacy Policy

Cookie Policy