Skip to Content
Course content

185: Existential Types with any Keyword Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember working with a junior dev a few years back who was building a dashboard for a fintech app. He had a Widget protocol and several structs—StockTicker, AccountBalance, and NewsFeed—that all conformed to it. He tried to create a list of widgets for the home screen using var widgets: [some Widget] = []. He was baffled when the compiler screamed at him, because he knew every item he was adding was, indeed, a Widget. He didn't realize that by using some, he was telling Swift that the array contained one specific (though hidden) type, rather than a mixture of different types. He wanted a bag of widgets, but he had accidentally asked for a bag of identical clones.

The Distinction Between a Specific Type and a Box

This is where we revisit the any keyword. When you use some Widget, you're dealing with an opaque type. The compiler knows exactly what the type is at compile time; it's just hiding that detail from you. But when you use any Widget, you're creating an existential type. Think of any as a physical box. Inside that box is some instance that conforms to Widget, but the box itself is the type. Because the box is the same regardless of what's inside, you can put a StockTicker box and a NewsFeed box into the same array.

protocol Widget {
    func render() -> String
}

struct StockTicker: Widget {
    func render() -> String { "AAPL: $150" }
}

struct NewsFeed: Widget {
    func render() -> String { "Swift 6 is coming!" }
}

// This works because 'any' allows heterogeneity
let dashboard: [any Widget] = [StockTicker(), NewsFeed()]

I'll be honest: in the early days of Swift, we just used the protocol name itself (e.g., [Widget]), and the compiler implicitly treated it as an existential. But that caused a lot of confusion between generics and existentials. Adding the any keyword makes it explicit that you are opting into "type erasure." You're telling the compiler, "I don't care about the specific underlying type anymore; just give me the protocol interface."

The Cost of the Existential Box

You might wonder why we don't just use any everywhere to make things easier. There's a price to pay. Every time you wrap a value in any, Swift has to manage a "Widget Table" (similar to a vtable) to keep track of the underlying type's methods. This adds a small amount of overhead. More importantly, you lose the ability to use Self or associatedtype requirements in a straightforward way. If your protocol has an associated type, using it as an any type can get messy because the compiler no longer knows which specific version of that associated type is inside the box.

I generally advise my team to stick with some (generics) whenever possible. If you're passing a dependency into a function, use some. If you're returning a specific type but want to hide the implementation, use some. Only reach for any when you truly need a collection of different types or when the type of an object needs to change dynamically at runtime.

Handling Existentials in Logic

Once you've stored something as any Widget, you can call any method defined in the Widget protocol. However, if you suddenly need to access a property that only exists on StockTicker, you have to "unbox" it using a conditional cast. It's a bit like reaching into the box and checking if the object inside is what you think it is.

for widget in dashboard {
    print(widget.render()) // Works: render() is in the protocol
    
    if let ticker = widget as? StockTicker {
        print("This is specifically a stock ticker!")
    }
}



📋 Practical Task

Implementing a Heterogeneous Notification System

You are building a notification system where different types of alerts (Email, SMS, and Push) must be processed in a single queue. Currently, the system is failing because it tries to use a generic constraint where an existential type is required.

Your Task:

  1. Define a protocol NotificationAlert with a method send() that returns a String.
  2. Create three structs—EmailAlert, SMSAlert, and PushAlert—that conform to this protocol, each returning a unique string (e.g., "Sending Email...", etc.).
  3. Create a class called NotificationManager. Inside this class, implement a property called queue that can hold a mixture of any NotificationAlert.
  4. Implement a method processAll() in the manager that iterates through the queue and prints the result of send() for every alert.
  5. Instantiate the manager, add one of each alert type to the queue, and call processAll().
Rating
0 0

There are no comments for now.

to be the first to leave a comment.