JackKelly / light-speed-io

Read & decompress many chunks of files at high speed
MIT License
58 stars 0 forks source link

`Typestate`: Use a custom type for each state (for each `IoOperation`) & use Rust's type system to make sure we only progress forwards #117

Closed JackKelly closed 6 months ago

JackKelly commented 6 months ago

Idea from the talk "Abusing the Type System for Fun and for Profit" at Rust Nation UK 2024. In particular, see slide 18 in the PDF slides.

This is called the "typestate pattern".

e.g. Rocket<Ground> vs Rocket<Air>. You convert from Rocket<Ground> to Rocket<Air> by launching Rocket<Ground> (and you can't launch Rocker<Air>).

Perhaps better, see this tutorial: https://cliffle.com/blog/rust-typestate/

JackKelly commented 6 months ago

A challenge is that typestate uses different types to encode different states of the same system (duh)... but we need to keep a Vec of these states! And we don't want to use Vec<Box<dyn TypeStateTrait>> because we want to minimise heap allocations.

Once Rust's const generics are stabilised then we may be able to use enum variants for the generic parameter of a struct. See this reddit thread for a code example. But maybe we still won't be able to store those in a Vec because those struct Foo<MyEnum::A> will still be considered a different type to struct Foo<MyEnum::B>?? Also, it's not clear when const generics will be stabilised.

So I think the answer is to use enums to represent state, and ensure we can only progress forwards. See this blog and the typestate_enum crate and slide 18 in the PDF slides.

JackKelly commented 6 months ago

So I think we want something like this, if it's allowed:

enum UringOperation<M> {
    GetRange {
        byte_range: UringOptimisedByteRanges<M>,
        fixed_file_descriptor: Option<usize>, // TODO: Use types::Fixed
        state: Enum {
            GetFilesizeAndOpen,
            Read,
            Close,
        },
    },
}

Or, maybe better:

enum UringOperation<M> {
    GetRangeGetFilesizeAndOpen,
    GetRangeRead,
    GetRangeClose,
}
JackKelly commented 6 months ago

As expected, this doesn't work:

struct Operation<S: IoState> {
    marker: std::marker::PhantomData<S>,
}

trait IoState {}
enum GetRangesRead {}
enum GetRangesClose {}
impl IoState for GetRangesRead {}
impl IoState for GetRangesClose {}

impl Operation<GetRangesRead> {
    fn foo() {}
}

impl Operation<GetRangesClose> {
    fn boo() {}
}

fn main() {
    let read_op: Operation<GetRangesRead> = Operation {
        marker: std::marker::PhantomData,
    };
    let close_op: Operation<GetRangesClose> = Operation {
        marker: std::marker::PhantomData,
    };

    let v = vec![read_op, close_op];  // Error: mismatched types
}

This doesn't work either:

let v: Vec<Box<Operation<dyn IoState>>> = vec![Box::new(read_op), Box::new(close_op)];

Also see: https://github.com/JackKelly/rust-playground/tree/main/typestate

JackKelly commented 5 months ago

I'm pretty happy with my current planned approach. See the lower half of the code here: https://github.com/JackKelly/light-speed-io/blob/new-design-March-2024/src/new_design_march_2024.rs