As far as I can tell, you can use different key types for slotmap
by just roundtripping through the data
method on Key
and the required From<KeyData>
implementation. It will be up to you to keep all the secondary map keys in sync with the primary node storage situation, but that's true even if you use the same key type.
It also means there's no real type safety encapsulation if you expose slotmap::Key
implementors to downstream: they can convert between your key types too.
Another challenge with this intuition is that the source of truth about a slotmap
key is the super case, so you would have to "downcast" your keys from NodeId
to SpanId
, etc., every time you got a new key. So you'll have to have some sort of "privileged" code (even if freely converting between all key types wasn't possible).
One way to approximate this would be to use a private enum
as the "base class", with private key types too, and only allow infallible "downcasting" where the slotmap is used directly.
new_key_type! { struct ImageKey; } // etc
enum NodeKey {
// Used for Default and insertions into Document.nodes
Bare(slotmap::KeyData),
Span(SpanId),
Image(ImageId),
}
impl From<slotmap::KeyData> for NodeKey { .. }
// delegate to variant payload
unsafe impl slotmap::Key for NodeKey { .. }
// Private "forceful downcasting" with more meaningful semantics
impl NodeKey {
fn to_image(self) -> ImageKey {
self.data().into()
} // etc
}
Then for the public interface, wrap them up and limit their interface.
// Public versions (private fields)
pub struct ImageId(ImageKey); // etc
pub struct NodeId(NodeKey);
// Public upcasting
impl From<ImageId> for NodeId { .. } // etc
// Fallible and public "downcasting"
impl TryFrom<NodeId> for ImageId { .. } // etc
Then on Document
you would do your downcasting where appropriate.
impl Document {
// private
fn insert_bare(&mut self, ..) -> NodeKey {
self.nodes.insert(..)
}
// Public. Consumers can use `.into()` to get a `NodeId`
pub fn insert_image(&mut self, ..) -> ImageId {
let ik = self.insert_bare().to_image();
self.images.insert(..);
ImageId(ik)
}
// You can take `NodeId` where appropriate, or use generics
pub fn do_node_stuff<Key: Into<NodeId>>(&mut self, key: Key) {
self.actually_do_node_stuff(key.into().0)
}
// This also reduces monomorph bloat (if you make `do_node_stuff`
// generic for ergonomic reasons).
fn actually_do_node_stuff(&mut self, key: NodeKey) {
// the actual logic
}
}
There actually is a way to do something like this with real subtyping, but it is very non-idiomatic, baroque, and leads to awful error messages, so I can't recommend it.