Type inference not working for inherited protocol

Hello,

I was wondering why the compiler can't compile the following code:

protocol Foo { }

struct FooBar: Foo { }

struct BarFoo: Foo { }

var children = [FooBar(), BarFoo()]

Both FooBar and BarFoo inherit from Foo, so I thought this would be simple to infer. I'm using Xcode 13.2.1, and apparently this is an error without a specific type annotation like:

var children: [Foo] = [FooBar(), BarFoo()]

Is this expected behavior, and if so, why? thanks

1 Like

This is expected behavior. Protocol conformance is not like class inheritance. Protocol conformance simply asserts that objects that conform to a protocol satisfy the requirements stated in the protocol. There is no relationship between the two struct types other than they both conform to the same protocol. To make an array of heterogenous types such as these, you have to treat them as members of the Foo family, which why the existential protocol type Foo has to be used to declare the children array. This has to be done explicitly. Otherwise, the compiler thinks you are trying to create an array of heterogeneous types, and can't infer what the type of the array should be.

3 Likes

So the main reason this doesn't compile is because we are trying to typecast existential types? If the shared type was a class, then this would work because these are concrete types. Why is typecasting to existential types problematic though?

To make it more explicit why it fails, consider this:

protocol Foo { }

struct FooBar: Foo { }

struct BarFoo: Foo { }

protocol Baz { }

extension FooBar: Baz { }

extension BarFoo: Baz { }

var children = [FooBar(), BarFoo()]

Now what is children ? Both [Foo] and [Baz] are valid and will work. So you need to specify which of them, or which other protocol they conform to, it is.

3 Likes

The compiler does not infer existential types (i.e. "protocol types") without a type annotation because it could be really unexpected. For example, if you had an array of Int values and one of them was accidentally UInt, you probably want an error telling you to construct an Int from the UInt instead of inferring the array element type as any FixedWidthInteger or some other protocol that both of these integer types conform to. It could also be very unexpected for protocols that a lot of types conform to that don't necessarily represent a sort of subtype relationship between these types, such as CustomStringConvertible. Other folks here have pointed out that there could also be type inference ambiguities if there are multiple protocols that this set of types conform to.

Finally, existential types have a lot of limitations that programmers should be aware of when using them, and an explicit type annotation is a signal that the programmer is intentionally using an existential type. This is exactly the motivation for adding an explicit keyword for these types in SE-0335.

8 Likes

To piggy-back on this: even in the nice circumstance where there's no ambiguity in the protocol to which all members conform, e.g.:

// Module 'Taxonomy'
public protocol Bird {}
public struct EmperorPenguin: Bird {}
public struct Dodo: Bird {}
public struct HarpyEagle: Bird {}

// Module 'Client'
// Suppose inference by conformance is allowed:
var birds = [EmperorPenguin(), Dodo()] // [any Bird], unambiguously
birds.append(HarpyEagle())             // OK: also a Bird

There's no guarantee that this promise is upheld—suppose Taxonomy expands:

// Module 'Taxonomy'
public protocol Flightless {}
extension EmperorPenguin: Flightless {}
extension Dodo: Flightless {}

What happens to this code in Client?

// Module 'Client'
var birds = [EmperorPenguin(), Dodo()] // [any Bird]? [any Flightless]? [any Bird & Flightless]?
birds.append(HarpyEagle())             // error, unless [any Bird] is somehow chosen

In other words: inference by conformance would allow normal evolution of a library to arbitrarily break type inference in its clients.

4 Likes