Compiler Error When Comparing 'some Protocol' Instances

I've been spending some time with Swift's opaque types trying to understand some of their nuances and I am running into some issues. 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, I believe I must specify the type for 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!

2 Likes

Before opaque types (pre-some) in Swift the type system worked by the rule that in the let x: <follows_after_:> expression that <follows_after_:> represents a concrete type which would mean the compiler has strong guarantees about what it can do with x and that it can combine it with any other y or z in any operation, provided all of them had the same string in their <follows_after_:>; These are generally referred to as complete types;

Protocols with associated-types or self-requirements are incomplete types so naturally you would get that dreaded error message Protocol P can only be used as a generic constraint because it has associated types or self requirements whenever you tried to declare such a variable;

Then opaque types came along and suddenly you could put the some keyword in the <follows_after_:> part;
This now changes the definition of <follows_after_:> to not represent concrete type here anymore but more generically type info here and it can either be a concrete type like in the beginning or an incomplete type by prepending some in front of a PAT protocol;

So when you write

let x: some Protocol

it's the same as if you would write

let x: unknown_type_but_conforms_to Protocol

thereby stripping the compiler of any strong guarantees about the underlying type of x; The compiler has to play it safe now and you may not combine x with any y or z in any operation even though they have the same string in <follows_after_:> i.e.

let y: some Protocol = ....
let z: some Protocol = ....

even though you as a programmer know that x, y and z have the same type because you get them from the same function.

Also the rule that with

func f() -> <f_return_type_info> {
  ....
}

the following declarations are equivalent

let x = f()
let y = f()
let x: f_return_type_info = f()
let y: f_return_type_info = f()

remains valid only for concrete / complete types in f_return_type_info

In reality it's not like the compiler transforms:

        let fetcher3 = FetcherFactory.getFetcher()
        let fetcher4 = FetcherFactory.getFetcher()

into:

        let fetcher3: some ContentFetcher = FetcherFactory.getFetcher()
        let fetcher4: some ContentFetcher = FetcherFactory.getFetcher()

but rather into something like:

        let fetcher3: unknown_but_complete_from_FetcherFactory.getFetcher = FetcherFactory.getFetcher()
        let fetcher4: unknown_but_complete_from_FetcherFactory.getFetcher = FetcherFactory.getFetcher()

And because of the constrictions -> some Protocol imposes on its functions the compiler that type is complete but unknown. Now the compiler knows that it can play safely and it can at least let you can combine fetcher3, fetcher4 into any operation because they have the same underlying type.

So here lies your confusion:

The only thing you're making specific is the fact that fetcher1 and fetcher2 have unknown_type_but_... types to the compiler;
Or in other words you're making non-specificness specific. I think this is because your mind is still accustomed with the old rule of what <follows_after_:> represents;

So nowadays
<follows_after_:> can mean complete type or incomplete type

<return_type_info> can mean complete type
or incomplete_type (if you specifiy incomplete_type in <follows_after_:>)
or complete_but_unknown_from_function***** (if you omit <follows_after:>)

5 Likes

Explanation above is great and reveals a lot of nuances. I just want to put it in more simple words, since I feel it might be a bit hard to understand.

Swift generally resolves types at compile-time (to make more performant runtime), and at the runtime there is no way to be sure that two some Equatable or any other protocols are using the same implementation type, since by using ˋsome` you mark that you do not care for exact type.

And what happens when you try to compare two Equatable instances? The compiler does not know if these instances are backed by the same type (and which one) and cannot resolve if it is possible to compare them at all. So from the point of the language it is meaningless operation to run this comparison, since first instance might be Int and second is ˋString. They both Equatable`, right? But you cannot compare them directly, you have to convert.

let lhs: some Equatable = "7"
let rhs: some Equatable = 7
assert(lhs == rhs)  // this would raise an error

I might be inaccurate in some details, but the general idea still would hold.

I believe this restriction is not really justified and should be lifted. We already allow functions to return other declaration's opaque return types, for example. What do you think @Joe_Groff ?

We don't want a declaration to end up with a different declaration's opaque type in its return type, though. Now that we support structural opaque types, though, maybe we could replace opaque types in the inferred property type with new opaque types tied to the property declaration.

I read through the thread, but still don't quite understand the behavior. @costinAndronache gives a hypothesis, but it's based on the assumption that type inference could lead to different behavior, which I doubt. There is a dedicated section in SE-0244 about type identity, so I suppose it must be an important feature of opaque type. But how could it be useful if it's so restrictive?