27 Apr 2024 - Sean Zhang
Why do we need erasing type infomation? Let’s dive deeper into some examples and use cases.
Let’s indroduce some basic types.
enum WeirdSize {
case humongo
case miniature
}
enum NormalSize {
case teacup
case teapot
}
struct Biscuit {
var size: WeirdSize
}
struct Tea {
var size: NormalSize
}
var eats: [Biscuit] = [Biscuit(size: .humongo)]
var drinks: [Tea] = [Tea(size: .teapot)]
// what if I want to combine eats and drinks array into one and what the type would it be?
var snacks: [Any] = eats + drinks
Here is a problem, how do we combine eats and drinks into one array since they don’t share anything in common?
Yes the var size
share the same name between Biscuit and Tea but their underlying type is different.
We can introduce a protocol Snackable
with associated type Size
and use the new swift feature existential any to combine them.
protocol Snackable {
associatedtype Size
var size: Size { get }
}
extension Biscuit: Snackable {}
extension Tea: Snackable {}
var snacks: [any Snackable] = eats + drinks
With existential type, there is a performance hit because it is using dynamic dispatch during the runtime. The next natural step is to use generic and type constraint to Snackable by using a wrapper type.
struct AnySnackable {
var snack: Any
init<T: Snackable>(_ snack: T) {
self.snack = snack
}
}
var snacks = [
AnySnackable(Biscuit(shape: .round, size: .humongo)),
AnySnackable(Tea(variety: .oolong, size: .teacup))
]
With Snackable
in the example above, it is certainly not ideal because we use the type Any
.
If we examine the Biscuit and Tea closer, we can see the associated type Size
can be erased from the array perspective.
So let’s try to erase the type Size
only.
Here we further refine the AnySnackable
protocol by adding a generic for the associated type Size
struct AnySnack<S>: Snackable {
var erasure: () -> S
init<T: Snackable>(_ concrete: T) where T.Size == S {
/// By wrapping the size property in a closure, we capture the concrete type T's Size type info and effectively erased all other type info of concrete T.
/// For example, the oreo and cheetos are treated as same type now and all other type info of oreo and cheetos are gone.
/// var oreo: AnySnack<WeirdSize> = .init(Biscuit(size: .humongo))
/// var cheetos: AnySnack<WeirdSize> = .init(Biscuit(size: .miniature))
self.erasure = {
return concrete.size
}
}
var size: S {
self.erasure()
}
}
let cookies: [AnySnack<WeirdSize>] = [
AnySnack(Biscuit(size: .humongo)),
AnySnack(Biscuit(size: .miniature)),
]
let teas: [AnySnack<NormalSize>] = [
AnySnack(Tea(size: .teacup)),
AnySnack(Tea(size: .teapot)),
]
// this will give you an error because WeirdSize and NormalSize doesn't match
// var snacks = cookies + teas
struct AnySizeSnack: Snackable {
typealias Size = Any
var erasure: () -> Size
init<T: Snackable>(_ concrete: T) {
self.erasure = {
return concrete.size
}
}
var size: Size {
self.erasure()
}
}
let oreos: [AnySizeSnack] = [
.init(Biscuit(size: .humongo)),
.init(Biscuit(size: .miniature)),
]
let greens: [AnySizeSnack] = [
.init(Tea(size: .teacup)),
.init(Tea(size: .teapot)),
]
var snacks = oreos + greens