How to use opaque types to solve problems

I hope that experts will come with their torches and illuminate this dark corner. :slight_smile:

Given the declaration:

protocol Bar {
   ...
}

func foo () -> some Bar
// foo's return value's type is an opaque type - always the same type that satisfies Bar

I understand the meaning of opaque types.

But, how do I use them to solve problems? Can you provide a practical example?

Edit: made the post and its title more specific.

They allow you to keep your interface free of concrete types while asserting type identity.

For example, if the function func foo() -> some Bar was exported by a library, the library would be able to change the underlying return type without changing the API of the function. We could do that with existentials as well, but because we know that calls to foo() will return the same type every time, we can also use them in places which require type identity, like when we bind them to an associated type.

It's all in the name "opaque type" - it's a type, and it has identity, but it's opaque so we don't know which specific type it is.

1 Like

Opaque types come handy when designing maintainable APIs. They hide complexity from the consumer while still keeping the concrete type underneath for the compiler to use (for type identity, static dispatching, optimizations etc)

In your example, the concrete return type of foo could be intricate: SomeBar<Result<User, ServiceError>>, and in practice it can get much more uglier than that, SwiftUI's func body is the best example.

So, as an API designer, one would like:

  1. To avoid offloading all that mental gymnastics to the consumer of your API, forcing them to write down the complete type definition when implementing a protocol for example.
  2. To keep a degree of flexibility and allow changing the concrete type without introducing breaking changes to your API (e.g. returning SomeOtherBar<Result<Profile, ServiceError>> instead wouldn't require a change in the function signature)

These would be the two problems you can solve with opaque types. They're pretty niche, but useful nonetheless.

1 Like

To be precise, the fact that every opaque type represents a single underlying type (and so has a particular identity based on how it was derived) is part of the language model and not just a detail used by the compiler.

Static dispatching and similar optimisations are orthogonal to opaque types - they are really more about inlining. You won't get them if your function is not inlinable with respect to the call site, and if the function is inlinable, you can get them even if the function returns an existential rather than an opaque type.

In other words, calling a function which returns some Collection<Int> and one that returns any Collection<Int> and using the returned value can have exactly the same performance if the functions are inlinable, because the compiler is allowed to look at the function body it is calling and punch through the abstractions.

2 Likes

i sometimes use these on computed properties when i have a gnarly LazyMapSequence or something similar that is unimportant to the caller of that property. but to be honest i don’t consider opaque return types to be an exceptionally high value feature, there is not a big difference between returning an opaque type and returning an existential and implicitly opening that existential near the call site.

1 Like