swiftgenericsswift-protocolsopaque-types

Can't assign value of 'some T' to type 'some T' in swift even if they are exactly the same, why?


I'm new to swift and is confused by how swift compiler treats the opaque type declarations for a variable.

For example, in the following minimum demo code:

protocol Animal {}

class Dog: Animal {}

func getMeADog() -> some Animal {
    return Dog()
}

var animal: some Animal = getMeADog()

@MainActor
func changeADog() {
    animal = getMeADog()
}

According to my understanding of swift's documentation of opaque types, opaque types (in this example some Animal) is a "wrapping" of a specific, compile-time inferred type which is known to the compiler.

If so, the compiler should've known that the animal is of type Dog since getMeADog() always return a Dog, causing animal to have underlying type Dog

Then, when I try to change a dog, which assigns another Dog (represented as some Animal) to the animal, the compiler should notice that they have the same underlying type (Dog), and complain nothing

However, the compiler DID complain about the type mismatch: Cannot assign value of type 'some Animal' (result of 'getMeADog()') to type 'some Animal' (type of 'animal')

What's more interesting is, when I remove the some Animal declaration for the animal variable, and let the compiler does type inference for me (which also produces a some Animal type shown in the IDE), the compiler NO LONGER complain on animal = getMeADog() line

Why is swift compiler behaving like this? What's the underlying difference between me specifying explicitly the type of the variable and letting the compiler do it for me?


Solution

  • The meaning of "a type known to the compiler" here is not "the compiler knows it is Dog". It just means that the compiler knows that it is a concrete type.

    You can think of some Animal as a fresh type variable that is a concrete type. The return type of getMeADog is not Dog, but a type that the compiler has "made up", just to represent the return type of getMeADog. Let's call this type TheReturnTypeOfGetMeADog.

    This is different from declaring the return type as any Animal, where the concrete type is unknown. With some Animal, the compiler knows that getMeADog will return the concrete type of TheReturnTypeOfGetMeADog every time. This allows you to do things that you can only do with a concrete type, e.g.

    struct Container<T: Animal> {
        var animal: T
    }
    
    func f() {
        Container(animal: getMeADog())
    }
    

    If getMeADog had returned an any Animal instead, the above would have produced an error, because Container<any Animal> is not valid. With some Animal, the compiler knows that TheReturnTypeOfGetMeADog is a concrete type, and so Container<TheReturnTypeOfGetMeADog> is a valid type.


    The reason why you are able to do:

    var animal = getMeADog()
    
    @MainActor
    func changeADog() {
        animal = getMeADog()
    }
    

    Because the compiler infers that the type of animal must be TheReturnTypeOfGetMeADog. The return type of getMeADog is also TheReturnTypeOfGetMeADog, so the assignment in changeADog is valid.

    On the other hand, if you write

    var animal: some Animal = getMeADog()
    

    then the type of animal is not TheReturnTypeOfGetMeADog. As I said before, some Animal is a fresh type variable. The type of animal is another type that the compiler has "made up", just to represent the type of the animal property. As far as the compiler is concerned, the type of animal is a different concrete type from TheReturnTypeOfGetMeADog. Note that these are types that the compiler has "made up" - at runtime, animal will of course just contain a Dog.

    The point of some types is that you can change the underlying implementation without breaking existing code. Changing the implementation of getMeADog to return a Cat should not break anything.

    If this were allowed:

    var animal: some Animal = getMeADog()
    
    @MainActor
    func changeADog() {
        animal = getMeADog()
    }
    

    The changing the initialiser of animal to, e.g. = Cat() would make the assignment animal = getMeADog() invalid.