Existential associated types

protocol Object {
    associatedtype Id
    var id: Id { get }
}

protocol Vehicle: Object {}

struct Car: Vehicle {
    struct Id { ... }
    let id: Id
}

Could we once be able to do this? Does this make any sense to you?

func registerVehicleId<T>(_ id: T.Id) where T: Vehicle {
    ...
}

func registerVehicleId(_ id: any Vehicle.Id) {
    ...
}

let vehicleId: any Vehicle.Id = car.id
registerVehicleId(car.id)
registerVehicleId(vehicleId)
1 Like

The existential version of this function would read to me as “registerVehicleId accepts any type which is a witness for Vehicle.Id in some visible conformance.” This seems… well founded to me at first blush but I’m not sure what you could usefully do with it, since there are no constraints on the Id associated type.

The generic version seems a bit trickier—since associated type witnesses aren’t unique, there’s no straightforward way to determine the identity of T in a call where you pass an arbitrary ID. E.g., if Car and Truck both conform to Vehicle with Id == String, the call to registerVehicleId("someID") would be ambiguous.

If the intention that the call above wouldn’t compile because "someID" wasn’t ‘derived’ from a vehicle conformance (i.e., it wasn’t the result of a call to Vehicle.id) I think you can make this sound, but it would require a lot of type system complexity and it’s not entirely clear what benefit it would bring.

Yeah, the associated type is meant to have requirements... For instance:
protocol Vehicle: Object where Id: LosslessStringConvertible {}

This way I would be able to process values, being sure that they are vehicle ids.

func registerVehicleId(_ id: any Vehicle.Id) {
    registerVehicle(rawId: String(describing: id))
}

You are right about the generic version! It doesn't seem to bring any benefit.

What advantage does this have over something like func registerVehicle(_ v: some Vehicle) with an implementation that calls v.id internally? Is it efficiency concerns about passing the entire Vehicle around, or something else?

Efficiency concerns about passing the entire Vehicle around, and also (and more importantly) that Vehicle isn't always available but only id.

The only constraint on Vehicle.Id is that it’s LosslessStringConvertible—what do you envision any Vehicle.Id to do other than being a synonym for any LosslessStringConvertible?

If I understand properly, it would only accept values which actually come from a Vehicle’s id property and not just arbitrary values which happen to be LosslessStringConvertible. Sort of another version of opaque types or dependent types.

1 Like

I don't know why would you want that, but with generalized existentials this and many other exotics things would be possible:

protocol Object {
    associatedtype Id
    var id: Id { get }
}

protocol Vehicle: Object {
}

struct Car: Vehicle {
    struct Id { ... }
    let id: Id
}

func registerVehicleId(_ id: any<T> T.Id where T: Vehicle) {
    let <V> openedID: V.Id = id
    print(V.self) // prints "Car"
    print(type(of: id)) // prints "Car.Id"
}

let car: Car = ...
registerVehicleId(car.id)

Note that such type would have a layout that stores value of identifier in the value buffer, but does store its type directly. Instead it stores types that existential type abstracts over - type T and witness table of T: Vehicle. When opening such existential you would get the full type T.

And when passing id to type(of:) it is casted to Any which has a different layout - value buffer and metadata for the actual type of the id. So type(of:) works as usual, but instead at the call site of type(of:) compiler would emit code that takes type metadata for T from existential, uses it to lookup metadata for the associated type, and then constructs Any with that metadata.

In this particular case, you can convert any Vehicle.Id to String, but not every String to some Vehicle.Id. I want to avoid runtime verifying if a string is actually some vehicle id. I want the type system to safeguard passing around values that are vehicle id types.

Currently I could do protocol Vehicle: Object where Id: VehicleId but VehicleId is arbitrary. The compiler won't stop anyone from writing extention Calendar: VehicleId {} and break the system. This solution is just artificial when there is a natural, strong enough relation established with the associated type.

How exactly would it break the system?

Yeah, I know what you mean. Good point!

But also, this is exactly how an artificial associating can go wrong. Is calendar VehicleId if you define it to be? Technically yes. And there is no vehicle that has an id of Calendar type. I would call it a broken system.

So, you requirement is that VehicleId need to have a corresponding a vehicle type, right? Is it a 1:1 relationship? Or the same Id can be reused with different vehicle types? How exactly do use vehicle type when processing ids?

If you don’t really use the vehicle type, I would suggest to not overthink it, and just use any VehicleId.

If you need Vehicle type and it is a 1:1 relationship, then having associated type for Vehicle inside VehicleId would solve the problem.

If you need Vehicle type and it is a 1 Id to many vehicles, then generics are you only choice for now. But I would love to learn more about your use case. If me or someone else ever gets to making an evolution proposal for generalized existentials, your use case could be used in the motivation section.

Yes. And I need Vehicle type.

I'm definitely working with concrete types like Car. In some places however I'm working only with Car.Id. These are often places where car isn't available, for instance

let reservationCarIds = reservations.map(\.carId)

And I don't want (and sometimes can't) go from car.id to car:

  • Efficiency concerns about fetching the entire Car from a server.
  • Efficiency concerns about passing the entire Car around.
  • Role based user can be restricted to access entire Car.

So I need to work those ids:
reservationCarIds.forEach { cancelReservationFor(carId: $0) }

But if there is a pattern in all vehicles, you naturally want to abstract these:
cancelReservation(for carId: Car.Id) { ... }
cancelReservation(for boatId: Boat.Id) { ... }
cancelReservation(for planeId: Plane.Id) { ... }
into cancelReservation(for vehicleId: any Vehicle.Id) since their implementation is likewise or even same. Notice any, since as Jumhyn pointed out a generic version could fail due to ambiguity.

You can't workaround the abstraction with cancelReservation(for vehicle: some Vehicle) because as I mentioned above, entire vehicle instance isn't available.


Initially I thought that existential associated types should be limited to types that have requirements to avoid weird notations like any Collection.Element but I'm not convinced yet.

If you define Vehicle.Id == String, then by construction every String is a Vehicle.Id. That is what == means. It sounds like you want Vehicle.Id to be a ‘’newtype” that wraps String, not == String.

I was referring to

where an arbitrary string can fail to produce a Car.Id via the failable initializer.


You are right. That is exactly how Car.Id is defined in the very first paragraph of this topic.

Isn’t the point that it should be the specific id type that this particular vehicle type uses? That makes sense even without any constraints.

What if their implementation is wrapped into a marker type?

protocol Vehicle { ... }

protocol VehicleId {}

struct ID<Entity, RawValue>: RawRepresentable {
    var rawValue: RawValue
}

extension ID: Hashable where RawValue: Hashable {}

extension ID: VehicleId where Entity: Vehicle {
    typealias VehicleType = Entity
}

cancelReservation(for carId: ID<Car, String>) { ... }
cancelReservation(for boatId: ID<Boat, String>) { ... }
cancelReservation(for planeId: ID<Plane, String>) { ... }

func cancelReservation<T: VehicleId>(for vehicleId: T) {
    print(T.VehicleType.self)
}

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... }
    }
}

I'm not really sure I understand the overall approach here. It sounds like you are modeling instances with a notion of identity, which furthermore have a subtyping relationship (cars and boats are all kinds of vehicles, and there are likely to be be vehicles that don't fall into any specific subtype) and for which you'd like to operate on heterogeneous collections. You even have efficiency concerns about passing instances around by value. This use case seems an exact fit for classes. Using a combination of protocols, existential types, associated values, generics, and runtime casts to re-create the same thing wouldn't be my go-to.

Would it make sense for there to be a VehicleID protocol with an associated type V: Vehicle where V.ID == Self? That would (I think) set up the unique relationship you're looking for between ID and Vehicle and allow you to recover the latter from the former.