When to use opaque types in variable declarations

My apologies for this first-grader question, but I need help with understanding the dreaded some. :slight_smile:

I have cannibalized the code in this this post to experiment:

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

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

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

struct S {
    let fetcher1: some ContentFetcher = FetcherFactory.getFetcher()
    let fetcher2: some ContentFetcher = FetcherFactory.getFetcher()
    
    func start() {
        let fetcher3 = FetcherFactory.getFetcher()
        let fetcher4 = FetcherFactory.getFetcher()
    }
}

The above code compiles.

Let me modify it slightly, by removing the some ContentFetcher from the declarations of the two properties:

struct S {
    let fetcher1 = FetcherFactory.getFetcher()
    let fetcher2 = FetcherFactory.getFetcher()

    func start() {
        let fetcher3 = FetcherFactory.getFetcher()
        let fetcher4 = FetcherFactory.getFetcher()
    }
}

Now, the property declarations don't compile, but the local variables do even though their declarations use the same pattern.

struct S {
    let fetcher1 = FetcherFactory.getFetcher() // Error
    // Property definition has inferred type 'some ContentFetcher', involving the 'some' return type of another declaration
    
    let fetcher2 = FetcherFactory.getFetcher() // Error
    // Property definition has inferred type 'some ContentFetcher', involving the 'some' return type of another declaration

    func start() {
        let fetcher3 = FetcherFactory.getFetcher() // Okay
        let fetcher4 = FetcherFactory.getFetcher() // Okay
    }
}

Can anyone explain why the local variables compile but the properties don't?

Also, this brings up another question: When and why should some be used in variable declarations?

From my understanding, the above example isn't about when to use opaque type, but about a specific behavior of type inference (when property's type is opaque type, it must be declared explicitly). Not sure if it's by design (from the error message it seems so).

As for whether/when to use opaque type for a property or local variable, I think it mainly depends on how it's initialized. In this specific case it's because getFetcher() returns an opaque type (note it's just a syntax shortcut for generic function), and hence the properties and local variables should follow suit.

Thank you, but how do you write that generic function?

This doesn't compile:

static func getFetcher <T:ContentFetcher> () -> T {
   return VideoFetcher ()
}

I probably shouldn't say it's a syntax shortcut, but it does the similar. In case you are not aware, opaque type was in generics roadmap and its SE document described it as reverse generics. The document even gave the above form as an example to understand it. In my understanding, the issue with the above form is that how would you declare the type on the caller side? You couldn't, because the return type isn't fixed. That's where opaque type comes into play.

1 Like

My guess is it's because the inferred unknown-but-same-from-getFetcher-type for fetcher3,fetcher4 is "materialized" only inside the start function, to allow you to operate on both of them based on their common type constraints( test them for equality because of their Equatable); i.e. it's not actually registered into Swift's type system;

What would you do if the compiler allowed you to declare fetcher1, fetcher2 like that?
You can't pass them into functions that process their unknown-but-same-from-getFetcher type because you have no means of representing such a type in code.

i.e You can't write:

func process(fetcher:  unknown-but-same-from-getFetcher) { ... }

But you could write:

func process(fetcher: some ContentFetcher) { ... }

and pass them into it because they are in fact some type of ContentFetcher , that's what getFetcher declares them as. There's no point for them to have stick with that inferred type because you can't use it anyway so thats why the compiler prefers you be explicit and put some ContentFetcher;

Furthermore what should the compiler do if you make them all public and produce a framework? How would the compiler represent fetcher1, fetcher2's types to the client code? It will have to materialize then the unknown-but-same-from-getFetcher type and register it into the type system but then that kinda defeats the purpose of opaque types, as they're all about not knowing what is the actual type of anything;

Swift is a very context-sensitive language on the semantic level; What is valid inside a function definition may not necessarily be valid in any other context even though you use the same construct / same syntax;

1 Like