iosswiftexistential-typeopaque-types

Compiler Error When Comparing `some Protocol` Instances


I've been spending some time comparing Swift's opaque types with existential/boxed types and trying to understand some of their nuances and I am running into some issues.

I understand that returning an existential type, compared to an opaque type, allows for the flexibility of using different dynamic types at runtime with the downside of i) some performance overhead and ii) some specific type information is lost.

The Swift Book documentation confirms the above and suggests existential types lose the ability to compare equatable types as a result:

Using a boxed protocol type as the return type for a function gives you the flexibility to return any type that conforms to the protocol. However, the cost of that flexibility is that some operations aren’t possible on the returned values. ... the == operator isn’t available — it depends on specific type information that isn’t preserved by using a boxed protocol type.

It then goes on to say that returning opaque types preserves type identity:

In contrast [to existential types], opaque types preserve the identity of the underlying type.

I decided to implement this myself to build an intuition around the tradeoffs between using opaque vs existential types. In doing so, I've encountered an issue I can't explain. I've narrowed down my issue to a very simple, albeit contrived, example.

I've defined a protocol ContentFetcher with a function fetch(). I also have a struct VideoFetcher that conforms to this protocol. Here's the code for these:

protocol ContentFetcher: Equatable {
    func fetch() -> Int
}

struct VideoFetcher: ContentFetcher {
    func fetch() -> Int {
        1
    }
}

I've then created a FetcherFactory struct that has a static function getFetcher() which returns some ContentFetcher:

struct FetcherFactory {
    static func getFetcher() -> some ContentFetcher {
        return VideoFetcher()
    }
}

However, when I try to create instances of ContentFetcher and compare them, I'm receiving compiler errors. Here's the code and the errors:

struct S {
    let fetcher1: some ContentFetcher = FetcherFactory.getFetcher()
    let fetcher2: some ContentFetcher = FetcherFactory.getFetcher()
    
    func start() {
        let fetcher3 = FetcherFactory.getFetcher()
        let fetcher4 = FetcherFactory.getFetcher()
        
        // Success: prints 'true'
        print(fetcher3 == fetcher4)
        
        // Error: Cannot convert value of type 'some ContentFetcher' (type of 'S.fetcher2')
        // to expected argument type 'some ContentFetcher' (type of 'S.fetcher1')
        print(fetcher1 == fetcher2)
        
        let fetcher5: some ContentFetcher = FetcherFactory.getFetcher()
        let fetcher6: some ContentFetcher = FetcherFactory.getFetcher()
        
        // Error: Cannot convert value of type 'some ContentFetcher' (type of 'fetcher6')
        // to expected argument type 'some ContentFetcher' (type of 'fetcher5')
        print(fetcher5 == fetcher6)
    }
}

S().start()

I understand that the opaque type returned by static func getFetcher() -> some ContentFetcher is used to hide the exact type of the return value. However, I cannot explain why fetcher3 == fetcher4 compiles successfully when both fetcher1 == fetcher2 and fetcher5 == fetcher6 generate errors.

It seems that when I let the compiler infer the opaque return type for fetcher3 and fetcher4 that the type identity is preserved and the compiler knows both types are the same. But if I make the opaque types explicit (let fetcher1: some ContentFetcher ...) then this identity is lost. Why is this when the return type of the getFetcher() method is still some ContentFetcher?

Also, the compiler forces me to specify the type for the opaque types used as stored properties; I get a compiler error if I remove some ContentFetcher from the type annotation of fetcher1 and fetcher2 (Error: Property definition has inferred type 'some ContentFetcher', involving the 'some' return type of another declaration). Why is this?

Any help would be much appreciated. Thanks!


Solution

  • Opaque result types are explained in SE-0244 Opaque Result Types. The part of interest to your question is Uniqueness of opaque result types:

    Opaque result types are uniqued based on the function/property/subscript and any generic type arguments.

    In this case, it means that the type of fetcher3 and fetcher4 are both (result of 'FetcherFactory.getFetcher()'). That, and its protocol, is the entirety of the information available at compile-time. Everything else about the type is "opaque." It is absolutely not VideoFetcher at compile-time, even though that is the actual concrete type returned. It is most importantly not compatible with other some ContentFetcher types, even if those types happen to be VideoFetcher. It is only compatible with (result of 'FetcherFactory.getFetcher()').

    some ContentFetcher is not a type at compile-time. It is replaced with a unique, nameless, compile-time type based on the assignment. This nameless type does not exist at runtime. At that point, only the full, concrete type exists. Opaque types are only a compile-time construct. (See Type Identity.)

    This is because you are free to change the implementation of getFetcher() to return something else, and that will not change anything about the callers. If somehow the fact that it really were VideoFetcher leaked out to the caller, then changing getFetcher would break calling code, and that's exactly what opaque types exist to prevent.

    As discussed in the Properties and subscripts section, an opaque property type is "the type of that property." There is no further information. The fact that there is a value assigned that also happens to be opaque doesn't change anything about that. The compile-time type of fetcher1 is literally (type of 'S.fetcher1'), as you see in your error messages. That is not compatible with any other type. Importantly, it is not the same as (type of 'S.fetcher2'). Opaque types are unique.

    As before, this allows you to change the specific type that fetcher1 is initialized with, without breaking callers. This is the point of opaque types.

    Just to reiterate based on your exact question:

    It seems that when I let the compiler infer the opaque return type for fetcher3 and fetcher4 that the type identity is preserved and the compiler knows both types are the same. But if I make the opaque types explicit (let fetcher1: some ContentFetcher ...) then this identity is lost. Why is this when the return type of the getFetcher() method is still some ContentFetcher?

    The compiler assigns the following types:

    Properties with the same type are comparable. Properties with different types are not.

    One question that may remain is "how do I express (result of 'FetcherFactory.getFetcher()') explicitly?" That is not currently possible. Opaque types are "nameless." There is no way to directly refer to them. They can only be referenced by type-inference. This creates some significant limitations. It's impossible to express that two functions return the same opaque type, for example. It's also impossible to express that fetcher1 and fetcher2 are the same type. (Opaque type inference is not allowed for properties.) See Opaque type aliases for possible future work that may improve this.