Type Erasures in swift
What is it and how does it solve the problem of equality check.
The problem:
As engineers, we encounter quite number of design problems in our day to day life. One such problem is enabling your protocol to be Equatable. We eventually want our domain layer to be as generic as possible. This means, we want our domain layer to contain abstract representations of object rather than a concrete type. This poses problem in some scenarios.
Consider you are building a vehicle inventory app, one of the core domain object requirement would be Vehicle
protocol Vehicle {var model: String { get }var brand: String { get }}
And you would want your `Vehicle` to equatable so that you can ensure that you’re not making any duplicate entries in your inventory system.
protocol Vehicle: Equatable {...}
Suddenly, the swift compiler will start throwing error on usages of Vehicle
saying,
Reasoning:
What does this error actually means? To understand it, lets look at the signature of ==
method of Equatable
static func == (lhs: Self, rhs: Self) -> Bool
The swift compiler simply can’t compare two objects for equivalence unless it knows the concrete types that conforms to the protocol. In real world, this protocol can be conformed by any number of struct/class/enums. So, it won’t make sense to compare them for equality just because all of the types conforms to the same protocol right?
But, what if you want to compare two objects of same concrete types? Thats practically a valid scenario, you would want to compare a Hatchback
(concrete) type with another Hatchback
and thats being prevented by swift compiler.
Solution: Type erasures
A Type erasure can be described as a type that erases the original type by encapsulating it in another concrete type that enables equality check. So basically, you create a concrete type EquatableVehicle
and make it conform the generic type Vehicle
and then you add an initialiser with a single parameter Vehicle
. This concrete type would act as a proxy and returns the underlying object’s properties when invoked.
struct EquatableVehicle: Vehicle {var model: String {return vehicle.model}var brand: String {return vehicle.brand}private let vehicle: Vehicleinit(_ vehicle: Vehicle) {self.vehicle = vehicle}}
Now, you need to add an additional method to our generic protocol, so that we can enable comparison and add a default implementation for Vehicle
types that conforms to Equatable
protocol Vehicle {var model: String { get }var brand: String { get }func isEqualTo(_ other: Vehicle) -> Bool}
extension Vehicle where Self: Equatable {func isEqualTo(_ other: Vehicle) -> Bool {guard let otherVehicle = other as? Self else { return false }return otherVehicle == self}}
And finally, you need to make our EquatableVehicle
to conform to Equatable
extension EquatableVehicle: Equatable {static func == (lhs: EquatableVehicle, rhs: EquatableVehicle) -> Bool {return lhs.vehicle.isEqualTo(rhs.vehicle)}}
So far, all our concrete types that conforms to Vehicle
and Equatable
will get a default implementation of func isEqualTo(_ other: Vehicle) -> Bool
, so does the EquatableVehicle
type. So when you compare two EquatableVehicle
types, the Equatable
conformance forward the validation to isEqualTo(...)
method. Where, the current object type(lhs) and passed in object type(rhs) are compared, if those two types are equal, then compare those objects for equality or return false, as we are trying to compare two different concrete types.
You could optionally add an additional convenience method to get the EquatableVehicle
type, just in case you want to compare it.
protocol Vehicle {//...func asEquatable() -> EquatableVehicle}extension Vehicle where Self: Equatable {//...func asEquatable() -> EquatableVehicle {return EquatableVehicle(self)}}
Time for testing:
struct Hatchback: Vehicle, Equatable {var model: Stringvar brand: String}struct Suv: Vehicle, Equatable {var model: Stringvar brand: String}let hundaiI20: Vehicle = Hatchback(model: "Hundai", brand: "i20")let marutiBaleno: Vehicle = Hatchback(model: "Maruti", brand: "baleno")let anotherHundaiI20: Vehicle = Hatchback(model: "Hundai", brand: "i20")let anSuv = Suv(model: "Maruti", brand: "baleno")print(hundaiI20.asEquatable() == anotherHundaiI20.asEquatable()) //Trueprint(hundaiI20.asEquatable() == marutiBaleno.asEquatable()) //Falseprint(hundaiI20.asEquatable() == anSuv.asEquatable()) //False
Resources:
You can find the related playground resource here.
Kindly share your thoughts about this article in the comments section.