I'm trying to write a generic token type which is the result of registration of resources. For example:
let mut registry_a = Registry::new();
let mut registry_b = Registry::new();
let token = registry_a.register::<SomeType>();
registry_b.instantiate(token) // this should fail to compile
Is such API even possible? I looked into invariant lifetimes (GhostCell) but all the implementations I saw used the with pattern which I don't know how to use for my use-case.
If someone can help me, I'll be very grateful. I'm also open to API changes in order to make my code harder to misuse.
Edit: I forgot to consider that register should be generic over the token.
Not-token-generic typestate idea
Maybe add a generic Registry argument to Token that is then set to some marker type MarkerA or MarkerB by the respective registry. RegistryB::instantiate then only allows you to pass Token<RegistryB> to it. Outline:
Do you mean instance or type? Because if you a token is linked to a certain registry instance, I would never return it to the caller but store it in the registry, only giving the user a unique key/index with which they could retrieve the token when needed. Calling methods that require the key with an invalid key (i.e. the registry doesn't have a token stored under that key) could fail at run time with an error or a panic.
Based on the wording[1], the OP does not want to rely on runtime errors /panics. They want to prevent at compilation time a Token instance from being passed to a Registry instance that did not create/register it.
I don't see how the OP could achieve that even if Rust were to have dependent types[2]. The closest thing I could think of is define Registry and Token to be type constructors instead of simple types/nullary type constructors. Then the OP needs to define a macro that auto generates a random type to pass to the Registry type constructor.
This seems rather insane though, and there will always be the chance that the auto-generated type collides with a previous one defined; however that would simply cause compilation to fail.
For example something like this:
use core::marker::PhantomData;
/// The only way to construct an instance of this is via
/// the macro [`gen_reg`]. `T`, for all intents and purposes, is
/// unnameable as it will be randomly defined.
struct Registry<T>(PhantomData<fn() -> T>);
impl<T> Registry<T> {
fn register(&self) -> Token<T> { … }
fn instantiate(&self, token: Token<T>) { … }
}
/// Randomly generates a type, `T`, that will be used for
/// `Registry` and `Token` before constructing an instance
/// of the constructed `Registry<T>` type.
macro_rules! gen_reg { … }
/// Returned from [`Registry::register`].
struct Token<T>(PhantomData<fn() -> T>);
fn main() {
// The type of `registry_a` is inaccessible to downstream code
// since the type passed to `Registry` was randomly generated
// which we need since we must prevent downstream code
// from creating two instances of the same type.
let registry_a: Registry<_> = gen_reg!();
let registry_b: Registry<_> = gen_reg!();
// We don't know what `_` is, but we _do_ know it's
// the same as the `_` for `registry_a` _and_ not
// the same as the `_` for `registry_b`.
let token: Token<_> = registry_a.register();
// This won't compile since the type passed to `Token` is
// not the same as the type passed to `Registry`.
registry_b.instantiate(token);
// The below two lines do compile though.
registry_a.instantiate(token);
// Multiple tokens can be registered still.
registry_a.instantiate(registry_a.register());
}
Should be noted that I know essentially nothing about macros, so I'm only "guessing" such a macro could be defined. I imagine macros can access something like rand::thread_rng().r#gen() to generate a random u64 to append to Marker which will be used as the name of the type that is defined.
The comment in their code says "this should fail to compile". ↩︎
My experience with dependent types is limited, so please correct me if I'm wrong. ↩︎
The slot_map crate has a feature where a macro can be (optionally) used to create a key type for each map, which prevents using the keys for one map in a different map. See: https://docs.rs/slotmap/latest/slotmap/#custom-key-types
That's once again a different type, rather than instance.
In my experience, this is impossible to achieve statically. I was trying to use it to provide safe deallocation, but unfortunately it's not possible in current Rust. Your only choice is to either differentiate the types, or use runtime validation.
It is possible with a GhostCell like approach, but that requires consumers to put everything in a closure (which the OP rejected). I don't know of any other ways.