diff --git a/SUMMARY.md b/SUMMARY.md index 9128dba8..8523438d 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -32,6 +32,7 @@ - [RAII Guards](./patterns/RAII.md) - [Prefer Small Crates](./patterns/small-crates.md) - [Strategy](./patterns/strategy.md) + - [Typestate/Session Types](./patterns/typestate.md) - [Contain unsafety in small modules](./patterns/unsafe-mods.md) - [Visitor](./patterns/visitor.md) diff --git a/patterns/typestate.md b/patterns/typestate.md new file mode 100644 index 00000000..cda3ade9 --- /dev/null +++ b/patterns/typestate.md @@ -0,0 +1,96 @@ +# The Typestate Pattern + +## Description + + +The typestate pattern is an API design pattern that encodes information about an object's run-time state in its compile-time type. In particular, an API using the typestate pattern will have: + +* Operations on an object (such as methods or functions) that are only available when the object is in certain states, + +* A way of encoding these states at the type level, such that attempts to use the operations in the wrong state fail to compile, + +* State transition operations (methods or functions) that change the type-level state of objects in addition to, or instead of, changing run-time dynamic state, such that the operations in the previous state are no longer possible. + +## Example + +This is one example of how to implement the pattern for a simple type that needs to be initialized or otherwise prepared before use +(that is, it has two states: (), and Ready). More states and operations may be added to implement a more complex state machine. + + +```rust + +use std::marker::PhantomData; + +pub struct Ready; + +pub struct Thing { + // tracks state type info at compile time, optimized out for runtime. + marker: PhantomData +} + +// Private constructor to internally control what state the struct is in. +fn state_constructor() -> Thing { + Thing { marker: PhantomData } +} + +// Operations in our default state () +impl Thing { + pub fn new() -> Self { + Self { marker: PhantomData } + } + + // Consumes the struct to return one with a new type state + pub fn get_ready(self) -> Thing { + state_constructor::() + } +} + +// Operations available in any state +impl Thing { + pub fn do_any_time(&self) { + println!("We can do this function whenever"); + } +} + +// We can only use this function when ready +pub fn do_only_when_ready(_: Thing) { + println!("We can only do this when we are Ready") +} + +fn main() { + let thing = Thing::new(); + + // Not ready yet + thing.do_any_time(); + // do_only_when_ready(thing); // this won't compile + + // Transition to Ready + let ready = thing.get_ready(); + + // Now we're ready + ready.do_any_time(); + do_only_when_ready(ready); +} +``` + + +## Motivation + +You are modelling a system that functions as a state machine, and want to ensure at compile-time that invalid states never occur in any runtime scenario. + +## Advantages + + +* It moves certain types of errors from run-time to compile-time, giving programmers faster feedback. +* It interacts nicely with IDEs, which can avoid suggesting operations that are illegal in a certain state. +* It can eliminate run-time checks, making code faster/smaller. + +## Disadvantages + +* It can add some verbosity and complexity to the code. +* Implementing it for complex structs can be difficult. +* It can make compiler error messages very hard to understand. + +## See also + + - [The Typestate Pattern in Rust, Cliff L. Biffle (2019)](https://web.archive.org/web/20210103081241/https://cliffle.com/blog/rust-typestate/)