Rusty Typestates – Starting Out
rustype.github.io
rustype.github.io
He covers a few additional parts of the typestate pattern, such as isolating data in specific states as well as sharing common implementations across a subset of states.
I'd also like to note that typestates also show up in functional programming under Indexed Monads, where a function might take a struct from an initial typestate, unwrap its data, and return a final(likely different) typestate. You can search Indexed Monad for more explanation there. If you work primarily in typescript you can find a production ready implementation of typestate programming here: https://github.com/DenisFrezzato/hyper-ts
I believe both have some overlap.
fn land(self) -> Drone<Idle> {
Drone::<Idle>::from(self)
}
instead of: fn land(self) -> Drone<Idle> {
Drone::<Idle>::new()
}
Otherwise the position assertions in drone_flies fail, because it's looking at the position of a brand new Drone<Idle>, rather than one based on the position you just flew to.Edit:
Yes it should. [1]
[1] https://github.com/rustype/drone/blob/main/src/drone.rs#L53
However, the article mentions:
The attentive reader may have noticed that the consumption and conversion of self into other types implies the values are moved (and in some cases, copied) around.
This is untrue; those moves will definitely be optimized away and the typestates will end up being zero cost.
The fact that Rust debug builds are unreasonably slower than C debug builds is a pet peeve of mine. And unoptimized moves are a big part of the problem. Or saying otherwise: "optimizing out" simple moves shouldn't be the job of an optimizer that sometimes doesn't run. It should be always done!
Of course the language semantics do still leave the door open for this optimization to happen some time in the future
Wow that's worse than I thought. This means that any pub function that does this pattern of taking ownership then giving it back should have at least #[inline] to enable inlining across crates, or maybe even #[inline(always)].
The difference to C to C++ is that in Rust, such pattern is sometimes necessary to work around the borrow checker.
Besides LLVM improvements, I suspect the moves might be optimized out at the MIR level, though that's just a hunch.
However for completeness, below is the article's example, where you can see it inlines pretty much everything as you'd expect. But even if you make it so it won't inline functions (e.g. add println to them) it still optimizes the moves away, which is good.
Essentially, instead of the compile-time value attached to a binding only being a type (with generics, optionally), it could also have a regular Rust value attached to it.
You've just reinvented dependent typing and all of its glory and challenges.
Scanner.OpenScanner sOpen = Scanner.open(/*some source*/);
//do some processing
Scanner.ClosedScanner sClosed = sOpen.close();
sClosed.nextLine() //type error
I sometimes do this in combination with the Builder pattern. This "typesafe builder" allows for a fluent API that guides the user as to its use.The point is, you can still use types to represent states.
Resource<Scanner> scannerResource = ...
scannerResource.use( scanner =>
... // use the scanner
)
After the closing parenthesis the .close() is called automatically.This allows for a lot of nice things, such as providing methods for stacking and combining resources, thus making sure they automatically close in the correct order. Callers never have to care about closing resource explicitly. Error handling is much easier to do.
Etc.
Here's a runnable example: https://scastie.scala-lang.org/5klYiw6cT7yckJbn2zGmJg
The gist is: this does everything needed for resources, including preventing errors at compile-time, being composable/reusable and handling errors correctly (you can throw an exception in the middle and the scanners will still be closed).
That being said, having it as a language feature might make certain use-cases nicer and more ergonomic. However, it also makes the language much more complex; I don't think resources alone are a sufficient reason to fundamentally change the language.
One thing that Rust has gotten fairly right so far is not having added too many special cases and rely on general language features mostly. For example, don't have special syntax/handling for optional/nullable values, but just use the existing macrosystem to deal with it. I hope this philosophy will continue.
Rust is here to stay and these kind of decisions make a big difference in the long run.