I've tried to find a minimalistic example of type erasure. From my experience, it often gets more complex, and I try to avoid it as much as possible. But sometimes that's the way.
It is finally the same complexity as before, with old-school language. Excepted that old-school language was hurting you by crashing, while swift hurts you at build time.
It is meant to be a strongly typed language, so it does not fit well with generics.
Suppose you need to manage some shapes in a document.
These shapes are Identifiable
s, meaning they have an id which type is determined by an associated type. Int in this case.
The code below won't build because it can't use the Shape
protocol directly since the type of id
is an associated type that is defined by object conforming to the Shape protocol
import Foundation
protocol Shape: AnyShape, Identifiable {
var name: String { get }
}
struct Square: Shape {
var id: Int = 0
var name: String { "Square" }
}
func selectShape(_ shape: Shape) {
print("\(shape.name) selected")
}
By adding a type erased shape, you can then pass it to functions.
Thus, this will build:
import Foundation
protocol AnyShape {
var name: String { get }
}
protocol Shape: AnyShape, Identifiable {
}
struct Square: Shape {
var id: Int = 0
var name: String { "Square" }
}
func selectShape(_ shape: AnyShape) {
print("\(shape.name) selected")
}
Simple use case.
Suppose now our app connects to two shapes manufacturers servers to fetch their catalog and sync with our's.
We know shapes are shapes, all around the world, but the ACME Shape Factory index in database is an Int
, while the Shapers Club use UUID
..
That's at this point we need to 'recover' the type, as you say.
It is exactly what's explained when looking in the AnyHashable
source doc.
Cast can't be avoided, and it is finally a good thing for the security of the app and the solidity of the models.
The first part is the protocols, and it may be verbose and become complex as the number of situations grows, but it will be in the communication foundation framework of the app, and should not change very often.
import Foundation
// MARK: - Protocols
protocol AnyShape {
var baseID: Any { get }
var name: String { get }
}
// Common functions to all shapes
extension AnyShape {
func sameObject(as shape: AnyShape) -> Bool {
switch shape.baseID.self {
case is Int:
guard let l = baseID as? UUID , let r = shape.baseID as? UUID else { return false }
return l == r
case is UUID:
guard let l = baseID as? UUID , let r = shape.baseID as? UUID else { return false }
return l == r
default:
return false
}
}
func sameShape(as shape: AnyShape) -> Bool {
return name == shape.name
}
func selectShape(_ shape: AnyShape) {
print("\(shape.name) selected")
}
}
protocol Shape: AnyShape, Identifiable {
}
extension Shape {
var baseID: Any { id }
}
The second part is the models - this will hopefully evolve as we work with more shape manufacturers.
The sensitive operation that can be done on shapes are not in this code. So no problem to create and tweak models and apis.
// MARK: - Models
struct ACME_ShapeFactory_Model {
struct Square: Shape {
var id: Int = 0
var name: String { "Square" }
var ACME_Special_Feature: Bool
}
}
struct ShapersClub_Model {
struct Square: Shape {
var id: UUID = UUID()
var name: String { "Square" }
var promoCode: String
}
}
Test
let shape1: AnyShape = ACME_ShapeFactory_Model.Square()
let shape2: AnyShape = ShapersClub_Model.Square()
let shape3: AnyShape = ShapersClub_Model.Square()
Compare two different shapes references from different manufacturers
shape1.sameObject(as: shape2) : false
-> Logic, it can't be the same item if it comes from different manufacturers
Compare two different shapes references from same manufacturers
shape2.sameObject(as: shape3) : false
-> This is a new shape, sync in catalog
Compare two identical shapes references from same manufacturers
shape2.sameObject(as: shape2) : true
-> We already have this one in the catalog
Compare two shapes from different manufacturers
shape1.sameShape(as: shape2) : true
-> Dear customer, we have two kind of squares from two manufacturers
That's all. I hope this may be of any help.
Any correction or remark is welcome.
Last word, I am quite proud of the name of my Shapes manufactures :)