Although primary associated types do help smooth out many of the edges of using certain existential types, there are still use-cases for manual type erasure using concrete Anyβ¦
types.
Existential types dynamically dispatch methods which their interface declares down to the underlying value, but critically, they cannot themselves:
- Conform to protocols
- Implement methods
- Satisfy static type requirements
A very common example of this is Equatable
conformance. We can update the Car
protocol to adopt Equatable
conformance, to indicate that Car
s should be able to be equated:
protocol Car<Fuel>: Equatable {
associatedtype Fuel
func drive(fuel: Fuel)
}
struct SportsCar: Car { β¦ }
struct FamilyCar: Car { β¦ }
struct WorkCar: Car { β¦ }
However, although you can check for whether two Car
values are equal if you know their static types, you cannot check two any Car
values for equality:
WorkCar() == WorkCar() // β
true
let electricCars: [any Car<Electricity>] = [WorkCar(), WorkCar()]
electricCars[0] == electricCars[1]
// π Type 'any Car<Electricity>' cannot conform to 'Equatable'
// Only concrete types such as structs, enums, and classes can conform to protocols
// Required by referencing operator function '==' on 'Equatable' where 'Self' = 'any Car<Electricity>'
Equatable
has a Self
requirement which any Car
cannot satisfy; however, you could do this if you wrote your own AnyCar
type:
struct AnyCar<Fuel>: Car {
private let inner: any Car<Fuel>
private let isEqual: (AnyCar<Fuel>) -> Bool
// The key to the `Equatable` conformance is capturing the _static_ type
// of the value we're wrapping.
init<C: Car<Fuel>>(_ car: C) {
inner = car
isEqual = { anyCar in
guard let otherCar = anyCar.inner as? C else {
return false
}
return car == otherCar
}
}
func drive(fuel: Fuel) {
inner.drive(fuel: fuel)
}
static func ==(_ lhs: Self, _ rhs: Self) -> Bool {
lhs.isEqual(rhs)
}
}
With this wrapper, you can then check two arbitrary AnyCar
values for equality:
let electricCars: [AnyCar<Electricity>] = [AnyCar(FamilyCar()), AnyCar(WorkCar()), AnyCar(WorkCar())]
electricCars[0] == electricCars[1] // β
false
electricCars[1] == electricCars[2] // β
true
This approach may look familiar to you in the usage of AnyHashable
as a generalized key type for dictionaries which can contain any types of keys. You could not implement the same with any Hashable
:
let d: [any Hashable: Any] = ["hi" : "there"] // π Type 'any Hashable' cannot conform to 'Hashable'
As opposed to AnyCar
, AnyHashable
has the benefit of being so prevalent and necessary that the compiler automatically wraps up types in AnyHashable
so you don't need to do it yourself, making it largely invisible.