This design is a thought experiment of mine, which has its origins in the ongoing discussion of the ECS (Entity Component System) design in Rust. The basic idea behind ECS is:
- Entities are owned at the top level, usually by a
Vec<T>
. - Indices into the
Vec
are used to reference a given entity within complex data structures.
This design works really well with Rust's borrow checker, because of the single ownership structure. Since indices alone don't allow read or write access to the data, Rust's borrow checker doesn't consider them aliases. It does have a couple drawbacks:
- If an entity is removed from the Vec without cleaning up all existing index references, then those indices are now stale, and may end up pointing at a different object, breaking application logic (but not causing any UB).
- It's difficult to assert that all index references to a given entity have been removed from nested data structures.
- Slots in the Vec now need to be managed to avoid fragmentation and unbounded growth.
- The design arguably "works around the borrow checker" instead of leveraging its capabilities.
In general, the first point can be mitigated through the use of "generational indices", but the others are still problematic.
As I thought about this problem, I focused on two particular points:
- Indices are just a way to encode a reference to a structure.
- The problem with a direct Rust reference is that it implies some permission to read/write the referenced data, which is why the borrow checker is involved.
My solution is to decouple a direct reference from the ability to access the data it references. This new kind of reference, which I called a Ticket
, keeps the referenced data live but cannot be used to access it directly. Instead, access is granted only through a single logical "owner" of the data, which provides an interface to borrow a value (immutably or mutably) given a Ticket
for that value. I called this owner a TicketMap
. A Ticket
is issued by a TicketMap
, and branded so that only that TicketMap
can be used to manipulate the ticket's value.
Ticket
is Clone
and internally contains a Rc
which tracks how many copies of the Ticket
are alive. If the last Ticket
referring to a particular value is dropped, then the value itself is dropped. Ticket
s can also be turned into the TicketMap
, which drops them. If the last Ticket
for a value is turned in, the value is returned.
This system solves the problems listed above with ECS:
- It's impossible for a
Ticket
to end up referring to the wrong value. - It's possible to assert, via
TicketMap::turn_in
, that no more tickets refer to a given value. - No management of slots is necessary; the TicketMap doesn't actually store the values in any contiguous or indexable structure.
- The design works with the borrow checker and feels more "Rust-like".