Well there are some defects. For instance, the compiler won't stop me to write ID<Car, Int>
in one place and ID<Car, String>
in another. And it does't allow for existential any Vehicle.ID
. Also, I need that id: ID
property on Object
. And feels cumbersome overall. It has an idea behind, though!
protocol Object where ID: RawRepresentable, ID.RawValue == String {
associatedtype ID
var id: ID { get }
}
protocol Vehicle: Object {}
@propertyWrapper
struct DetachedID<T> where T: Object {
let wrappedValue: T.ID
}
extension Object {
var detachedID: DetachedID<Self> {
DetachedID(wrappedValue: id)
}
}
func registerVehicle<T>(with id: DetachedID<T>) where T: Vehicle {
print("Registered \(T.self) id: \(id.wrappedValue)")
}
Fabulous result:
struct Car: Vehicle {
struct ID: RawRepresentable {
let rawValue: String
}
let id: ID
}
let car = Car(id: Car.ID(rawValue: "car:0"))
registerVehicle(with: car.detachedID) // Registered Car id: "0"
There is, however, a major problem and that is lacking existential vehicle id.
let anyVehicleID: DetachedID<any Vehicle> // Type 'any Vehicle' cannot conform to 'Object'
This could be probably easily resolved with extending existential types as extension any Vehicle: Vehicle { ... }
but until then I guess I need the next-gen form of type erasure.
struct AnyVehicle: Vehicle {
struct ID: RawRepresentable {
let rawValue: String
}
let boxed: any Vehicle
var id: ID {
// Cannot convert value of type 'Any' to expected argument type 'String'
// Wasn't it [advertised to know the type](https://forums.swift.org/t/unlock-existential-types-for-all-protocols/40665)?
ID(rawValue: boxed.id.rawValue as! String)
}
}
extension Vehicle {
var detachedVehicleID: DetachedID<AnyVehicle> {
DetachedID(wrappedValue: AnyVehicle(boxed: self).id)
}
}
Now I can go register a list of vehicle ids.
struct Boat: Vehicle {
struct ID: RawRepresentable {
let rawValue: String
}
let id: ID
}
let boat = Boat(id: Boat.ID(rawValue: "boat:0"))
let vehicleIDs = [
car.detachedVehicleID,
boat.detachedVehicleID
]
vehicleIDs.forEach {
registerVehicle(with: $0)
}
So yes, there is a solution, a strong one if you want (for what was the assignment). But it still feels more like workaround than solution. Either what I call existential associated type or I believe generalized existentials you mentioned would do better.
But I'm afraid what I initially pitched needs to be more narrowed, with the established relationship between types more concrete to avoid nonsenses like any Sequence.Element
. Maybe something like inner associatedtype A
that would extend the requirement for Self.A
to be inner type..., or to allow arbitrary types to be related but with explicit spelling like dependent associatedtype
... I don't know, I really don't know to think like a compiler engineer.
With inner associated type though, more possibilities are emerging, thinking out loud - default type implementations. Every adopter of Object
gets the default inner type for free if it doesn't define one itself:
extension Object {
struct ID: RawRepresentable, Decodable {
// Don't allow initializing from arbitrary string by default.
init?(rawValue: String) {
nil
}
let rawValue: String
init(from decoder: Decoder) throws { ...decode string and assign to rawValue... }
}
}