So I fell for the typestates meme and tried to use them to encode nontrivial things like “what if this function can transition the state to multiple possible valid states instead of just a Result” and basically you end up with some very horrifying code that uses a lot of generic parameters, requires a lot of ceremony when matching over the possible transitions, etc. If you’re interested you can see the typestate code here, where it models a basic lexer:
In general, the approach to make sure only relevant member functions are available should be the norm. If you can foolproof your API, do it! If a file is opened as read-only, there should be no write() function. Stdin should not have a rewind() function. Distinguish between the type of path, e.g. FilePath or DirectoryPath (or Path<File> and Path<Directory>) so you can prevent File::open(path.parent()). Etc.
Using Typestates is syntactic sugar. One could also use different types. The approach with the prettiest, but foolproof, API is the best one.
The method of the article to add a templated member also allows to do inheritance in Rust.
The article does not show what the example would look like with the typestates. Perhaps they do not want to show the need for introducing a new binding or hiding the old ones:
fn read_contents_of_file(path: &Path) -> Result<String, Error> {
let my_file = MyFile::open(path)?;
// The initial my_file must be consumed so the compiler can check
// that it cannot be read from after an error.
let (my_file, result) = my_file.read_all()?;
my_file.close();
}
There’s also some recent work done on using the type state pattern in Rust to provide some guarantees on crash consistency for a file system by enforcing a specific ordering.
I think this article was where I learned about typestates in Rust 6 years ago. See also…
My link log also points to the classic 1986 paper Typestate: a programming language concept for enhancing software reliability
So I fell for the typestates meme and tried to use them to encode nontrivial things like “what if this function can transition the state to multiple possible valid states instead of just a
Result” and basically you end up with some very horrifying code that uses a lot of generic parameters, requires a lot of ceremony when matching over the possible transitions, etc. If you’re interested you can see the typestate code here, where it models a basic lexer:https://codeberg.org/tlaplus/rstla/src/commit/c2fcb7431622fceb873d4a1829341881ff92a4d0/src/lexer_state.rs
Here’s the code that’s using that code, which admittedly I managed to make look fairly nice (exempting the preamble):
https://codeberg.org/tlaplus/rstla/src/commit/c2fcb7431622fceb873d4a1829341881ff92a4d0/src/lexer.rs#L104
I wonder whether this would have become viable if I knew how to write Rust macros.
In general, the approach to make sure only relevant member functions are available should be the norm. If you can foolproof your API, do it! If a file is opened as read-only, there should be no
write()function.Stdinshould not have arewind()function. Distinguish between the type of path, e.g.FilePathorDirectoryPath(orPath<File>andPath<Directory>) so you can preventFile::open(path.parent()). Etc.Using Typestates is syntactic sugar. One could also use different types. The approach with the prettiest, but foolproof, API is the best one.
The method of the article to add a templated member also allows to do inheritance in Rust.
The article does not show what the example would look like with the typestates. Perhaps they do not want to show the need for introducing a new binding or hiding the old ones:
Good example.
This is possible because Rust is a resource-affine language:
https://en.wikipedia.org/wiki/Substructural_type_system#The_resource_interpretation
(Admittedly, it’s a terrible name for the kind of move semantics that Rust has.)
There’s also some recent work done on using the type state pattern in Rust to provide some guarantees on crash consistency for a file system by enforcing a specific ordering.