Note that because only the Start constructor is public End can only be obtained by stepping through the states. My question now is if we have a function that takes the start state and returns the end state, how can we enforce that the End actually comes from the given Start and not from another builder?
// should be valid
fn build1(start: Start) -> End {
start.next().next()
}
// should be invalid
fn build2(start: Start) -> End {
Start(0).next().next()
}
Apparently this can be enforced by doing some lifetime / variance acrobatics:
Since I am not well-versed in such acrobatics I am wondering a couple of things:
How could such tricks be applied to achieve the above with a typestate builder?
What's the difference between struct A<'a>(PhantomData<fn(&'a ()) -> &'a ()>) and struct B<'a>(PhantomData<&'a mut &'a ()>)? (I have to admit that I don't really understand the implications of either)
What are the drawbacks to using something like this in a library? Can such tricks be assumed to work reliably with future versions of Rust?
You would need to annotate the objects with a lifetime and ensure that the lifetime is invariant.
Nothing. It's just two different ways to make the lifetime invariant.
Putting lifetimes on stuff can lead to various challenges, e.g. you can't use APIs that require 'static. It is guaranteed to work in future versions of Rust.
No you can't set the lifetime to static because the goal is to avoid mixing up elements with two different lifetimes. You can't do that if they all have the same lifetime.
The compact_arena crate appears to define a tagged! macro for creating new unique lifetimes.