How are `Array` and `Value` covariant

I just noticed that Array and Value are covariant, but general generic placeholders are invariant:

class Base {}
class Derived: Base {}
struct Foo<Value> {}

// Ok
let array: Array<Base> = Array<Derived>()

// Fail
let foo: Foo<Base> = Foo<Derived>()

You can even down cast [Base] to [Derived] using as! with appropriate values.

Is this a special behaviour, or is there a way to annotate it?

4 Likes

The type checker hardcodes conversions from Array<T> to Array<U> if there is a conversion from T to U. Similar rules exist for Optional and Dictionary. There's no mechanism for doing this with your own types.

9 Likes

Why does let foo: Foo<Base> = Foo<Derived>() fail?
I was totally expecting it to compile.

Because they are treated as entirely unrelated types. As Slava explained, the covariance we have is special-cased for a few hand-picked standard library types only.

I guess then my question is, why are they treated as entirely unrelated types? :sweat_smile:

Because for a generic type Foo<T>, the compiler doesn’t know how the type Foo uses T.

Are you reading Ts from your Foo? If so, you’ll be reading Base from something that actually contains a Derived. That’s fine.

But what if you’re storing (i.e. setting) Ts in to your Foo? Then you’ll be able to set a value of type Base in to something that expects a Derived. That’s not fine.

(To be clear: when you write let foo: Foo<Base> = Foo<Derived>(), the dynamic type of foo is Foo<Derived>. That’s what it “really” is, and what determines is layout, etc. The static type is Foo<Base>, which is the interface you want to use to interact with it. You’re using it as if it was a Foo<Base>)

6 Likes

Because it is not sound to treat them as interchangeable types, or to treat one as a subtype of another, in the general case. For example, suppose you have this very simple generic container:

class Foo<T> {
  var t: T
}

Now this function takes a Foo<Base> and stores a new instance of Base in there:

func takesFooBase(_ foo: Foo<Base>) {
  foo.t = Base()
}

And this function takes a Foo<Derived> and prints out foo.t, which must be an instance of Derived since T is Derived:

func takesFooDerived(_ foo: Foo<Derived>) {
  print(foo.t)
}

Now imagine you could do this:

let fooDerived = Foo<Derived>()
let fooBase: Foo<Base> = fooDerived

takesFooBase(fooBase)
takesFooDerived(fooDerived)

What would happen?

6 Likes

Somebody asked about this on Stack Overflow and then I came here and saw this. Is there a more general solution for conversion than what I thought of there, that people use? I thought of that while I was asleep and it doesn't seem that usable.

Consider the following:

1 Like

Great example, thank you :pray:t3:

I guess I was expecting the compiler to somehow change the dynamic type of fooDerived to Foo<Base> upon assignment to fooBase. But that would break fooDerived: Foo<Derived>.

Terms of Service

Privacy Policy

Cookie Policy