Give your simulator superpowers

Give your Xcode
Simulator extra features

Existential any in Swift explained with code examples

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:

  1. Consider starting with concrete types first, don’t overcomplicate from the start
  2. Move to opaque types using the some keyword once you need more type flexibility
  3. Change some to any 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 the ImageFetching 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!

 

Featured SwiftLee Jobs

Loading RSS Feed

Browse more Swift related Jobs, or add your own on SwiftLee Jobs