Understanding Opaque Types

I am having trouble understanding opaque types. What is the difference between createFood and alsoCreateFood? Is there a reason to prefer one return type over the other? If so, why?

protocol Food {}
struct DogFood: Food {}

func createFood() -> [some Food] {
    [DogFood(), DogFood()]
}

func alsoCreateFood() -> some Sequence {
    [DogFood(), DogFood()]
}

It depends on what "knowledge" you want to provide to or hide from callers.

This returns an Array. The elements in the array are of some concrete type that conforms to Food. The actual element type (DogFood) is not known to the caller.

This returns some concrete type that conforms to Sequence. Neither the concrete type of sequence (Array) nor the element type of the sequence (DogFood) are known to the caller. The caller doesn't even know that the elements conform to Food. For all the caller knows, this function could return a Set<Int> or a Dictionary<String, Int> or …. The only thing that is known to the caller is that the returned type will be the same every time the function is called (this is a property of opaque result types).


In most cases, the first variant [some Food] is probably more useful because it gives the caller information about the element type. Without knowing anything about the element type, callers probably won't be able to do much with the elements.


Here's another desirable variant that should work but unfortunately doesn't in Swift 5.10:

// Error: 'some' types are only permitted in properties, subscripts, and functions
func createFood3() -> some Sequence<some Food> {
    [DogFood(), DogFood()]
}

This claims to return some concrete sequence type (the actual type is unknown, could be an array or set or …) where the elements all have the same concrete type that conforms to Food, but the actual type is unknown to the caller. Unfortunately, this doesn't compile. See Nested constrained opaque result type example from SE-0346 doesn't compile · Issue #61409 · apple/swift · GitHub.

Lastly, another variant:

struct CatFood: Food {}

func createFood4() -> some Sequence<any Food> {
    [DogFood(), CatFood()]
}

Here, some Sequence<any Food> says that the elements in the sequence are boxed values (also called "existentials"). Each value inside the boxes conforms to Food, but now it's not guaranteed that all those values have the same underlying type. So the sequence can contain both (boxed) DogFood and CatFood values at the same time.

This was also possible with your second variant (alsoCreateFood) because it didn't put any constraints on the element type, but not with your first because [some Food] guarantees that every element in the array has the same concrete type (the caller just doesn't know what it is).

7 Likes

Thanks a lot for the detailed explanation. I have been trying to find my way though Documentation and it has been quite a ride but your explanation is a perfect companion to that. Thanks again.

1 Like

Yes, it's tricky. In my experience, the most in-depth documentation about language features is in the Swift Evolution proposals. They're your best bet if you really want to understand things.

The Swift Evolution dashboard I've linked to above has a search field that lets you filter the proposals to find the relevant ones. For opaque result types, your entry point would be SE-0244: Opaque Result Types.

3 Likes