Skip to content

Commit 8a48392

Browse files
committed
Add Functional Programming - Stateful Types
1 parent fb57f21 commit 8a48392

File tree

2 files changed

+248
-0
lines changed

2 files changed

+248
-0
lines changed

SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444

4545
- [Functional Programming](./functional/index.md)
4646
- [Programming paradigms](./functional/paradigms.md)
47+
- [Stateful Types: Generics as Type Classes](./functional/stateful-types.md)
4748

4849
- [Additional Resources](./additional_resources/index.md)
4950
- [Design principles](./additional_resources/design-principles.md)

functional/stateful-types.md

+247
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Stateful Types: Generics as Type Classes
2+
3+
## Description
4+
5+
Rust's functional roots allow for it to be more expressive in the type system
6+
than many other languages, and turn many kinds of programming problems into
7+
"static typing" problems. A key part of this idea is the way generic types work.
8+
9+
In C++ and Java, for example, generic types a meta-programming construct for the
10+
compiler. A `Vec<int>` and `Vec<char>` in C++ are just two different copies of
11+
the same boilerplate code for a `Vec` type, with two different types filled in.
12+
13+
In Rust, the generic type parameter creates what is known as a "type class", and
14+
each value used by an end user *actually changes the type of each instatiation*.
15+
In other words, `Vec<usize>` and `Vec<char>` *are two different types*.
16+
17+
This is why `impl` blocks must specify generic parameters: different ones can
18+
have different `impl` blocks on them.
19+
20+
It is recommended in rust to use generic types to enforce invariants. The best
21+
example is a state machine.
22+
23+
## Example
24+
25+
Suppose you are designing an interpreted language runtime, which requires a JIT
26+
compiler in order to process. Many Rust novices coming from other languages
27+
would implement it this way:
28+
29+
```rust
30+
#[derive(Debug)]
31+
pub enum CompileError {
32+
// error type, std::error::Error impl skipped for brevity
33+
}
34+
35+
#[derive(Debug)]
36+
pub enum ExecError {
37+
// error type, std::error::Error impl skipped for brevity
38+
}
39+
40+
pub type ExecutionResult = Result<(), ExecError>;
41+
42+
pub type CompileResult = Result<(), CompileError>;
43+
44+
pub enum Param {
45+
GCSize(usize),
46+
MaxStack(usize),
47+
// more parameters...
48+
}
49+
50+
#[derive(Default)]
51+
pub struct Interpreter {
52+
// state and execution data
53+
}
54+
55+
impl Interpreter {
56+
/// Set a system prameter.
57+
///
58+
/// # Panics
59+
/// Will panic if called after a script is executed.
60+
///
61+
pub fn set_param(&mut self, p: Param) {
62+
/* update the state based on the parameter */
63+
}
64+
65+
/// Set up a new execution environment.
66+
///
67+
/// # Panics
68+
/// Will panic if called more than once or after a script has been compiled.
69+
///
70+
pub fn init(&mut self) {
71+
/* Create the heap, prepare for use... */
72+
}
73+
74+
/// Load and compile a script. Returns any errors encountered.
75+
///
76+
pub fn compile_script(&mut self, script: &str) -> CompileResult {
77+
/* actually do the compile... */
78+
Ok(())
79+
}
80+
81+
/// Execute all scripts added. Returns any errors encountered.
82+
///
83+
/// # Panics
84+
/// Will panic if zero scripts have been compiled.
85+
///
86+
pub fn exec(&mut self) -> ExecutionResult {
87+
/* actually run the script... */
88+
Ok(())
89+
}
90+
}
91+
92+
fn main() {
93+
let mut interp = Interpreter::default();
94+
interp.set_param(Param::GCSize(1024 * 1024 * 1024));
95+
interp.init();
96+
interp.compile_script("print('2 + 2')").unwrap();
97+
interp.exec().unwrap();
98+
}
99+
```
100+
101+
Why those chances to panic? Because there are *state invariants* here:
102+
103+
1. `set_param` can only be called in an "initial" state.
104+
1. `init` must be called before any scripts are compiled.
105+
1. `exec` can only be called in a "loaded" or "ready" state.
106+
1. `init` must be called *exactly once*.
107+
108+
It would be possible to add these to the `Error` types instead of panicking.
109+
However, this solution is suboptimal. Not only would users of the code
110+
correctly have to `unwrap()` a result all the time, but the requirement to call
111+
init exactly once is still unenforcable across an entire program without
112+
additional state (like `called = true` in the struct).
113+
114+
What would really be neat is if it were possible to create a compile-time error
115+
if it were misused. After all, every user's program contains the invalid call
116+
order in the logic itself.
117+
118+
In Rust, this is actually possible! The solution is to *change the type* in
119+
order to enforce the invariants. How? With a private generic parameter.
120+
121+
Here is what that looks like:
122+
123+
```rust,ignore
124+
// this is a module to prevent users outside this crate from doing their own impls
125+
mod state_trait {
126+
pub(crate) trait State {}
127+
128+
pub(crate) struct Init {
129+
params: Vec<Param>,
130+
}
131+
impl State for Init {}
132+
133+
pub(crate) struct Loaded {
134+
scripts: Vec<CompiledScript>,
135+
}
136+
impl State for Loaded {}
137+
138+
pub(crate) struct Ready(Vec<CompiledScript>);
139+
impl State for Ready {}
140+
}
141+
use state_trait::*;
142+
143+
144+
struct Interpreter<S: State> {
145+
// same fields for execution, but now add:
146+
state_data: S,
147+
}
148+
149+
impl Default for Interpreter<Init> {
150+
/* impl does the same thing the old default one did */
151+
}
152+
153+
impl Interpreter<Init> {
154+
/// Set a system prameter.
155+
///
156+
/// # Panics
157+
/// Will panic if called after a script is executed.
158+
///
159+
pub fn set_param(&mut self, p: Param) {
160+
/* update the state based on the parameter */
161+
}
162+
163+
/// Initialize this intepreter, disallowing more parameter sets.
164+
///
165+
pub fn init(self) -> Interpreter<Loaded> {
166+
/* copy all of the parameters into self's members... */
167+
168+
// return our new initialized interpreter
169+
Interpreter {
170+
state_data: Loaded { scripts: vec![] },
171+
// copy other fields...
172+
}
173+
}
174+
}
175+
176+
impl Interpreter<Loaded> {
177+
/// Load and compile a script. Returns any errors encountered.
178+
///
179+
pub fn compile_script(&mut self, script: &str) -> CompileResult {
180+
/* actually do the compile... */
181+
Ok(())
182+
}
183+
184+
// Indicates we are done compiling scripts.
185+
pub fn ready(self) -> Interpreter<Ready> {
186+
/* prepare to actually execute scripts... */
187+
188+
Interpreter {
189+
state_data: Ready(),
190+
// copy other fields...
191+
}
192+
}
193+
}
194+
195+
impl Interpreter<Ready> {
196+
/// Execute all scripts added. Returns any errors encountered.
197+
///
198+
/// # Panics
199+
/// Will panic if no scripts had been compiled.
200+
///
201+
pub fn exec(&mut self) -> ExecutionResult {
202+
/* actually run the script... */
203+
Ok(())
204+
}
205+
206+
/// Returns to a state where more scripts can be loaded.
207+
pub fn reset(self) -> Interpreter<Loaded> {
208+
/* clean up any state as needed... */
209+
210+
Interpreter {
211+
state_data: Loaded(self.0),
212+
// copy other fields...
213+
}
214+
}
215+
}
216+
```
217+
218+
With this approach, if the user were to make a mistake and set a parameter
219+
after init:
220+
221+
```rust,ignore
222+
fn main() {
223+
let mut interp = Interpreter::<Init>::default().init();
224+
interp.set_param(Param::GCSize(1024 * 1024 * 1024);
225+
}
226+
```
227+
228+
They would get a syntax error. The type `Interpreter<Loaded>` does not
229+
implement set param, only the type `Interpreter<Init>` does.
230+
231+
## Disadvantages
232+
233+
This is a lot of typing. Depending on the amount of change caused by state
234+
transitions, an `InvalidState` enum value in an error type might be simpler.
235+
236+
## Alternatives
237+
238+
There are a number of simpler state machines, however, that have their own patterns:
239+
240+
1. If the state transition is during construction/finalizing of an object, see [Builder Pattern](../patterns/creational/builder.md).
241+
1. If the state transitions don't change invariants much, see [Strategy Pattern](../patterns/behavioural/strategy.md).
242+
243+
## See also
244+
245+
* [The Case for the Type State Pattern](https://www.novatec-gmbh.de/en/blog/the-case-for-the-typestate-pattern-the-typestate-pattern-itself/)
246+
* FIXME: I remember seeing a Rust talk which described this in more detail, but I
247+
can't find it again.

0 commit comments

Comments
 (0)