Existential any allows you to define existential types in Swift by prefixing a type with the any
keyword. In short, an existential means “any type, but conforming to protocol X.”
Any and AnyView have different purposes, which I explain in my article AnyObject, Any, and any: When to use which?. In this article, I’ll explain what existential types are, when to use them, and their impact on performance.
What does Existential any mean?
By using the any
keyword in front of a protocol, we’re defining an existential type of a specific protocol. For example, defining a variable of any type conforming to the Content
protocol:
protocol Content {
var id: UUID { get }
var url: URL { get }
}
struct ImageContent: Content {
let id = UUID()
let url: URL
}
let content: any Content = ImageContent(...)
SE-335 introduced existential any, and Swift 5.6 made it available first. Swift 5.7 enhanced existential and allows us to use existentials combined with associated types. For example, we could redefine our Content
protocol to inherit the Identifiable
protocol with a UUID
type constraint and use it as an existential accordingly:
protocol Content: Identifiable where ID == UUID {
var url: URL { get }
}
struct ImageContent: Content {
let id = UUID()
let url: URL
}
let content: any Content = ImageContent(...)
Constrained existentials
With the introduction of Primary Associated Types in SE-346, we can also define constrained existentials. To explain this further, I’d like to introduce an ImageFetching
protocol with a primary associated type Image
:
protocol ImageFetching<Image> {
associatedtype Image
func fetchImage() -> Image
}
The generic parameter in the protocol definition defines the primary associated type by matching the name of the associated type that has to become primary. In this case, we defined Image
to be our primary associated type.
We can use the primary associated type as a constraint in our code. For example, defining an extension on UIImageView
to configure an image using an image fetcher:
extension UIImageView {
func configureImage(with imageFetcher: any ImageFetching<UIImage>) {
image = imageFetcher.fetchImage()
}
}
Since UIImageView
requires a UIImage
type, we constrained the parameter type to be any kind of ImageFetching
, but having an associated type of UIImage
. In other words: we can use any type conforming to the ImageFetching
protocol, but it has to define UIImage
as its associated type.
Note: in the above example, it’s better to use some. I’ll explain this later in the performance section.
When to use Existentials
The earlier shared examples already gave insights into when you should use existentials. My general advice towards using existential any would be as follows:
- Consider starting with concrete types first, don’t overcomplicate from the start
- Move to opaque types using the some keyword once you need more type flexibility
- Change
some
toany
when you know you need to store arbitrary (random) values
To further explain the above steps, I would like to continue the story of image fetching. For example, we could create a remote image fetcher:
public struct RemoteImageFetcher: ImageFetching {
let url: URL
public func fetchImage() -> UIImage {
// ..
}
}
Followed by an image fetching factory returning an image fetcher for a given URL:
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> RemoteImageFetcher {
RemoteImageFetcher(url: url)
}
}
It’s the easiest to start with a concrete RemoteImageFetcher
type since there is no requirement yet to make it more dynamic.
The value of existentials for framework development
However, it could be that you’re developing a 3rd party library or SDK used by external parties. In that case, you only want to expose the minimum required APIs to prevent many breaking changes for implementors. For example, if we expose the RemoteImageFetcher
type and rename it to ExternalImageFetcher
, we need all implementors to update their implementation accordingly.
Instead, we could rewrite the code following step 2 of my advice to make use of the some keyword:
struct RemoteImageFetcher: ImageFetching {
let url: URL
func fetchImage() -> UIImage {
return UIImage()
}
}
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> some ImageFetching {
RemoteImageFetcher(url: url)
}
}
As you can see, we no longer have to define the RemoteImageFetcher
type to be publically visible. Instead, we return some ImageFetching
as API users don’t have to know the exact concrete type.
Lastly, we could introduce another image fetcher that will handle local images:
struct LocalImageFetcher: ImageFetching {
let url: URL
func fetchImage() -> UIImage {
return UIImage()
}
}
Once we update our factory method:
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> some ImageFetching {
if url.isFileURL {
return LocalImageFetcher(url: url)
} else {
return RemoteImageFetcher(url: url)
}
}
}
We will run into the following error:
Function declares an opaque return type ‘some ImageFetching’, but the return statements in its body do not have matching underlying types
As you might know from my article Some keyword in Swift: Opaque types explained, opaque types require to have a fixed type within the scope they’re used. Since we’re returning either a LocalImageFetcher
or a RemoteImageFetcher
the compiler can no longer statically determine the outcome type. In this case, you are required to define the return type to be existential:
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> any ImageFetching {
if url.isFileURL {
return LocalImageFetcher(url: url)
} else {
return RemoteImageFetcher(url: url)
}
}
}
In other words:
We’ll return
any
type conforming to theImageFetching
protocol
Since the compiler can no longer statically predict the returned type, we’ll have to accept some performance impact.
The impact of existentials on performance
As quoted from the SE-335 proposal:
This proposal makes the impact of existential types explicit in the language by annotating such types with
any
.
In other words, you’ll now know better when you’re defining a type to be existential. The reason for doing so is related to the impact of existentials on performance. They’re more expensive than concrete types since they can store any value conforming to the protocol and since they can dynamically change. To explain this, you can look at the following code example:
var anyContent: any Content = ImageContent(…)
anyContent = VideoContent(…)
The above code example compiles successfully and demonstrates the fact that anyContent
can change dynamically. Due to this, existential types require dynamic memory. They also incur pointer indirection and dynamic method dispatch that cannot be optimized away. Without going into details about what this means, we can conclude that it’s better not to use any
if you can.
As shared before, it’s better to start with concrete or opaque types first. You’ll notice soon enough when you need existentials. For example, the above code example can be rewritten using opaque types:
var someContent: some Content = ImageContent(…)
someContent = VideoContent(…)
The compiler would indicate dynamic type change isn’t possible with the some
keyword:
Cannot assign value of type ‘VideoContent’ to type ‘some Content’
Enforced starting from Swift 6
The performance impact of existentials used to be too hidden, so the Swift team decided to introduce the any
keyword. To not force us to change our complete code base to use any
where required, we’re only enforced to do so starting from Swift 6.
Conclusion
Existentials in Swift allow defining a dynamic value conforming to a specific protocol. Using primary associated types, we can constrain existentials to certain boundaries. The Swift team introduced the any
keyword to let developers explicitly opt-in to a performance impact that might otherwise not be visible.
If you like to improve your Swift knowledge, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!