Type-Level Programming in Rust
willcrichton.net
willcrichton.net
However, there's some really neat stuff later, so it's wise to grit your teeth and get past it.
In case anyone else had the same reaction, here's an entirely-safe version of the first snippet that you can pretend replaced the original:
// Each state is a unique type
struct Receiving;
struct Sending;
// The state machine is parameterized by the state
struct Channel<State> {
chan: ...,
_state: PhantomData<State>
}
// Methods for the state are uniquely associated with only the state
impl Channel<Receiving> {
// recv consumes ownership, ensuring old state is invalidated
fn recv(mut self) -> (Channel<Sending>, String) {
let Channel { chan, .. } = self;
let msg = chan.recv();
// The state type changes after executing a transition
let next = Channel { chan, _state: PhantomData };
(next, msg)
}
}
impl Channel<Sending> {
fn send(mut self, msg: String) -> Channel<Receiving> {
let Channel { chan, .. } = self;
chan.send(msg);
Channel { chan, _state: PhantomData }
}
}
#[test]
fn channel_test() {
let c: Channel<Sending> = Channel::new();
let c: Channel<Receiving> = c.send("hi");
let (c, msg) = c.recv();
// and so on
}Admittedly the SecureVec example is more understandable: because the generic is passed through to Item as well, you’ll need to convert the Vec<Item<T, ItemLevel>> to a Vec<Item<T, MaxLevel<ItemLevel, VecLevel>>>, which… well, it can be done readily enough without unsafe code, by consuming the Vec, mapping each item and collecting to a new vec, but short of a kind of specialisation magic that I don’t think will kick in in this case, that’s going to entail allocating a new vector, so that I would be inclined to use unsafe code here for efficiency; but care should still be taken that it doesn’t invoke undefined behaviour.
But one more word in defence of the transmutes: it makes it obvious that it’s either zero-cost or just a copy, whereas more involved things can become subject to compiler optimisations, so that you don’t know that the whole supposedly-noop transformation will be being optimised out of existence. That confidence and clearly-stated intent is valuable.
There are some deficiencies, most prominently that the “role” system is aggressively monomorphic, so you can’t always prove that two things are coercible—the compiler must conservatively assume that type parameters with unknown roles cannot be coerced—but overall it’s much better than what we had before, and I expect it’ll continue to improve.
It cites the Haskell work: https://github.com/jswrenn/rfcs/blob/safer-transmute/text/00...
I’m thinking about a similar sort of coercion system for a language project, and it’s good to have a slightly broader view of the design space. This area hasn’t been very well explored yet, not just in terms of semantics, but also developer ergonomics: most of the time you want to hide representations for convenience, but when considering safe coercions, you really want to make it possible to talk about them explicitly & precisely.
This feature is also very subtle, and right at the very edge of the type system, so it’s extremely easy to break soundness here, usually by failing to consider things like variance. The reason “roles” were added to GHC in the first place isn’t just because of the issue with type families / associated types mentioned in that RFC; it’s also because anything that provides a safe API atop unsafe internals could also be used to violate soundness, especially in conjunction with mutation. The classic examples are: 1. coercing “container of A” to “container of B” using the coercion from A to B, then inserting a B that is not a valid A, either in its value or just because A and B have incompatible trait instances (like ordering or hashing). You need to be able to disallow conversions between things even if they have identical representations.
I’m pretty sure that declaring both `Item` and `SecureVec` as `#[repr(transparent)]` would cover that last 0.0001% chance: it protects against the extra fields becoming non-zero-size by accident and forces the binary representation to be identical to the first field.
At that point, he’s transmuting between things with the same representation; all that’s left to verify is semantics.
You can avoid the overhead without transmute, but it's still unsafe and not easily explained: https://github.com/Munksgaard/session-types/commit/3a310d205...
Since `transmute` will be undefined behaviour if the layout differs you will only be saving cost if you would be invoking undefined behaviour.
I recommend checking the generated asm and reporting a missed-optimization bug if it differs.
By applying a pattern that should rightly raise alarm bells for any seasoned Roast coder, you've distracted from your goal. By linking to a Reddit discussion rather than editing your post to clarify it, you've done your readers a disservice by drawing them into this muck.
fn cast<E2, P2>(self) -> Chan<E2, P2> {
Chan(self.0, self.1, PhantomData)
}In general, this series of posts has been more about types than about rust so I don't blame him for ignoring unsafe. It isnt relevant (its also not a best practice!)
Say no more! Any brother of such renowned talent is sure to know what they're talking about.
The README uses some of the same motivating examples as the blog post.
Type-level programming absolutely has its place. Rust's `typenum` and `generic_array` crates enabled projects like `nalgebra` long before const generics. I recently used type-level programming to prototype an extension to Rust's type system — that's now an RFC (#2981)!
Finding the right balance between using type-level programming to ensure invariants and not using it is a important skill.
_state: PhantomData<State>
Since State is zero-sized it is free to store it in the struct. state: State
IIUC PhantomData is only required when you need to indicate that you are logically storing something of that type but don't want to actually store it. Since storing is free in this case you may as well just store it.Tried it in both Firefox and Chrome.