Swift 5.7 introduced primary associated types. While experimenting with this feature, I was wondering if there is still a use case for type erasure types like AnySequence or if primary associated types make those fully obsolete?
For example, if we have the following code:
protocol Car<Fuel> {
associatedtype Fuel
func drive(fuel: Fuel)
}
struct Electricity {}
struct Petrol {}
struct SportsCar: Car {
func drive(fuel: Petrol) { print("ποΈ") }
}
struct FamilyCar: Car {
func drive(fuel: Electricity) { print("π") }
}
struct WorkCar: Car {
func drive(fuel: Electricity) { print("π") }
}
We can now make an array with only electric cars:
let electricCars: [any Car<Electricity>] = [FamilyCar(), WorkCar()]
Previously I would have written something like this:
struct AnyCar<Fuel>: Car {
//Implementation
}
let electricCars: = [AnyCar(FamilyCar()), AnyCar(WorkCar())]
Are there still cases where a custom struct "AnyCar" would make sense?
Thank you!
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:
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.