Swift existential types (`any P`)

Mar 31, 2023#swift

Existential types (any P) is a feature of Swift 5.6 that allow you to use a protocol as a type, without knowing the concrete type that conforms to the protocol.

However, existential types have some limitations, such as not being able to use protocols with associated types or self requirements. To overcome these limitations, Swift 5.7 introduced constrained existential types, which allow you to specify constraints on the existential type using generic parameters or where clauses.

To use existential types in Swift, you need to prefix the protocol name with the any keyword. For example:

// Define a protocol called Animal that requires a makeSound() method
protocol Animal {
    func makeSound()
}

// Define a protocol called Pet that inherits from Animal and requires a play() method
protocol Pet: Animal {
    func play()
}

// Define a class called Dog that conforms to both Animal and Pet protocols
class Dog: Animal, Pet {
    func makeSound() {
        print("Woof!")
    }

    func play() {
        print("Fetch!")
    }
}

// Define a class called Cat that conforms to both Animal and Pet protocols
class Cat: Animal, Pet {
    func makeSound() {
        print("Meow!")
    }

    func play() {
        print("Purr!")
    }
}

// Define an existential type of any Animal by using the any keyword
let animal: any Animal

// Assign a Dog instance to animal
animal = Dog()

// Call the makeSound() method on animal without knowing its concrete type
animal.makeSound() // Woof!

// Define an existential type of any Animal that is also a Pet by using the & operator
let pet: any Animal & Pet

// Assign a Cat instance to pet
pet = Cat()

// Call both makeSound() and play() methods on pet
pet.makeSound() // Meow!
pet.play() // Purr!

The any keyword is different from the some keyword, which creates an opaque type that represents some specific type that conforms to a protocol. The some keyword is used to hide the concrete type from the caller, while the any keyword is used to accept any concrete type from the caller.

To open the existential box at runtime means to access the concrete type of an object that is wrapped in an existential type, you need to use a dynamic cast operator:

protocol Shape {
  var area: Double { get }
}

struct Circle: Shape {
  var radius: Double
  var area: Double {
    return .pi * radius * radius
  }
}

struct Square: Shape {
  var side: Double
  var area: Double {
    return side * side
  }
}

let shape: any Shape = Circle(radius: 5)

// To open the existential box at runtime, use a dynamic cast operator
if let circle = shape as? Circle {
  print("The shape is a circle with radius \(circle.radius)")
} else if let square = shape as? Square {
  print("The shape is a square with side \(square.side)")
} else {
  print("The shape is unknown")
}

Apart from using dynamic cast operators, there are some proposals and discussions to improve the support for opening existential types in Swift, such as: