0

I want to use the typestate pattern to define several states that allow some exclusive operations on each of them. I'm using traits instead of an enum to allow further customizations.

So, I'm able to use this pattern until I try to include it inside a struct (the Session part) that is mutated when files are added, changed or removed.

trait IssueState {}

struct Open;
impl IssueState for Open {}

struct WIP {
    elapsed_time: u32,
}
impl IssueState for WIP {}

struct Closed {
    elapsed_time: u32,
}
impl IssueState for Closed {}

struct Issue<T: IssueState + ?Sized> {
    state: Box<T>,
    comments: Vec<String>,
}

impl<T: IssueState> Issue<T> {
    pub fn comment<S: Into<String>>(&mut self, comment: S) -> &mut Self {
        self.comments.push(comment.into());
        self
    }
}

impl Issue<Open> {
    pub fn new() -> Self {
        Self {
            state: Box::new(Open),
            comments: vec![],
        }
    }

    pub fn start(self) -> Issue<WIP> {
        Issue {
            state: Box::new(WIP { elapsed_time: 0 }),
            comments: self.comments,
        }
    }
}

impl Issue<WIP> {
    pub fn work(&mut self, time: u32) -> &mut Self {
        self.state.elapsed_time += time;
        self
    }

    pub fn done(self) -> Issue<Closed> {
        let elapsed_time = self.state.elapsed_time;
        Issue {
            state: Box::new(Closed { elapsed_time }),
            comments: self.comments,
        }
    }
}

impl Issue<Closed> {
    pub fn elapsed(&self) -> u32 {
        self.state.elapsed_time
    }
}

struct Session<T: IssueState> {
    user: String,
    current_issue: Issue<T>,
}

impl<T: IssueState> Session<T> {
    pub fn new<S: Into<String>>(user: S, issue: Issue<T>) -> Self {
        Self {
            user: user.into(),
            current_issue: issue,
        }
    }

    pub fn comment<S: Into<String>>(&mut self, comment: S) {
        self.current_issue.comment(comment);
    }
}

impl Session<WIP> {
    pub fn work(&mut self, time: u32) {
        self.current_issue.work(time);
    }
}

trait Watcher {
    fn watch_file_create(&mut self);
    fn watch_file_change(&mut self);
    fn watch_file_delete(&mut self);
}

impl<T: IssueState> Watcher for Session<T> {
    fn watch_file_create(&mut self) {
        self.current_issue = Issue::<Open>::new();
    }
    
    fn watch_file_change(&mut self) {}
    fn watch_file_delete(&mut self) {}
}

fn main() {
    let open = Issue::<Open>::new();
    let mut wip = open.start();
    wip.work(10).work(30).work(60);
    let closed = wip.done();

    println!("Elapsed {}", closed.elapsed());

    let mut session = Session::new("Reviewer", closed);
    session.comment("It is OK");
    
    session.watch_file_create();
}

Rust Playground (original)

Rust Playground (edited)

What can I do to fix the problems?

Is the typestate pattern limited to only some situations that do not depend a lot on external events? I mean, I'm trying to use it for processing events, but is it a dead end?, why?

Deveres
  • 97
  • 7
  • Does https://stackoverflow.com/questions/33687447/how-to-get-a-reference-to-a-concrete-type-from-a-trait-object answer your question? – hkBst Jul 10 '22 at 14:05
  • You're losing all the benefits of the typestate if you hide it behind a dynamic type. The main benefit being able to check state transitions *at compile time* but if you need a `Session` to handle any state, you're probably better off using an enum. – kmdreko Jul 10 '22 at 14:35
  • @kmdreko, `Issue` can be used in a lot of places, so I want to have those benefits everywhere. I don't know a way to check the state at compile time when using enums. – Deveres Jul 10 '22 at 15:57
  • @Deveres you can't check state at compile time with enums, but nor can you with `dyn IssueState`. – kmdreko Jul 10 '22 at 15:59
  • That link is interesting, @hkBst, Thank you. I need some time to explore it – Deveres Jul 10 '22 at 16:02
  • In the first lines of `main`, there is the example of how the issue could transition to a different state, each of them with a different API. How could I do that with an enum? I mean, I can only think of matching it and panicking when trying to do something forbidden for the specific state. – Deveres Jul 10 '22 at 16:07
  • 1
    @Deveres yes, that's exactly what you have to do. If the compiler doesn't have your back because you don't know what state a `Session` has at compile time, then you must be able to handle cases when the state is not what is expected. That's what I meant when I said you're losing all the benefits of the typestate pattern if you hide it behind a dynamic type. – kmdreko Jul 10 '22 at 16:34

1 Answers1

0

Your Session has a Issue<dyn IssueState> member, but you want to implement its work method by calling Issue<WIP>'s work method. The compiler complains, because an Issue<dyn IssueState> is not (necessarily) a Issue<WIP> and so does not implement that method.

hkBst
  • 2,818
  • 10
  • 29
  • Yes, I understand that. I want to know if there is a way to fix or workaround this problem and keep something similar. – Deveres Jul 10 '22 at 16:10
  • @Deveres, well, you will have to explain what you want exactly. – hkBst Jul 10 '22 at 16:45
  • 1
    @Deveres is making `Session` generic `` an option? – kmdreko Jul 10 '22 at 16:59
  • Making `Session` generic works: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b0313111502797bdc892c8e24f27d43d. But, what could I do if I can't change that? – Deveres Jul 11 '22 at 10:58
  • @kmdreko, I've edited the question, to reflect additional problems when trying to use the generic `Session`. – Deveres Jul 11 '22 at 11:35