mdeloof / statig

Hierarchical state machines for designing event-driven systems
https://crates.io/crates/statig
MIT License
560 stars 18 forks source link

Difficulty getting generics in state to compile #8

Closed k3d3 closed 1 year ago

k3d3 commented 1 year ago

This is tangential to #7, but not completely related. I don't know if I'd consider it a bug, but it's a bit of a pain.

I've been trying to get a state machine working where I have a generic type in the state.

My initial attempt looks something like this:

use std::marker::PhantomData;
use statig::prelude::*;

pub struct Event;

pub enum State<T> {
    Init,
    Other(PhantomData<T>),
}

impl<T> statig::State<Machine> for State<T> {
    fn call_handler(
        &mut self,
        machine: &mut Machine,
        event: &Event,
    ) -> Response<Self> {
        todo!()
    }
}

#[derive(Default)]
pub struct Machine;

impl<T> statig::StateMachine for Machine {
    type Event<'a> = Event;

    type State = State<T>;

    type Superstate<'a> = ();

    const INITIAL: Self::State = State::Init;
}

fn main() {
    let machine = Machine::default().state_machine().init();
}

However I get the error:

error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates
  --> src/main.rs:26:6
   |
26 | impl<T> statig::StateMachine for Machine {
   |      ^ unconstrained type parameter

For more information about this error, try `rustc --explain E0207`.

Okay, fair enough. So I add to the Machine itself to constrain it:

use std::marker::PhantomData;
use statig::prelude::*;

pub struct Event;

pub enum State<T> {
    Init,
    Other(PhantomData<T>),
}

impl<T> statig::State<Machine<T>> for State<T> {
    fn call_handler(
        &mut self,
        machine: &mut Machine<T>,
        event: &Event,
    ) -> Response<Self> {
        todo!()
    }
}

#[derive(Default)]
pub struct Machine<T> {
    _marker: PhantomData<T>,
}

impl<T> statig::StateMachine for Machine<T> {
    type Event<'a> = Event;

    type State = State<T>;

    type Superstate<'a> = ();

    const INITIAL: Self::State = State::Init;
}

fn main() {
    let machine = Machine::<u32>::default().state_machine().init();
}

But now I get an even stranger error message:

error[E0309]: the parameter type `T` may not live long enough
  --> src/main.rs:31:27
   |
31 |     type Superstate<'a> = ();
   |                           ^^- help: consider adding a where clause: `where T: 'a`
   |                           |
   |                           ...so that the type `State<T>` will meet its required lifetime bounds

For more information about this error, try `rustc --explain E0309`.

If I change the line to this, as it suggests:

    type Superstate<'a> = () where T: 'a;

I now get a third different error:

error[E0276]: impl has stricter requirements than trait
  --> src/main.rs:31:39
   |
31 |     type Superstate<'a> = () where T: 'a;
   |                                       ^^ impl has extra requirement `T: 'a`

For more information about this error, try `rustc --explain E0276`.

I've tried several things to try getting around the two latter errors, but I've given up sadly. :(

I'm not sure how to get generics to work in state, apart from forking statig and removing every mention of Superstate, which is a very... hacky brute-force solution.

Do you have any ideas on things I could try? I've unfortunately run out of ideas.

mdeloof commented 1 year ago

Adding a T: 'static bound should do trick I think.

use statig::prelude::*;
use std::marker::PhantomData;

pub struct Event;

pub enum State<T> {
    Init,
    Other(PhantomData<T>),
}

impl<T> statig::State<Machine<T>> for State<T>
where
    T: 'static, // Added bound here ...
{
    fn call_handler(&mut self, machine: &mut Machine<T>, event: &Event) -> Response<Self> {
        todo!()
    }
}

#[derive(Default)]
pub struct Machine<T> {
    _marker: PhantomData<T>,
}

impl<T> statig::StateMachine for Machine<T>
where
    T: 'static, // ... and here.
{
    type Event<'a> = Event;

    type State = State<T>;

    type Superstate<'a> = ();

    const INITIAL: Self::State = State::Init;
}

fn main() {
    let machine = Machine::<u32>::default().state_machine().init();
}

As far as I understand this tells the compiler that T that can be safely held indefinitely long, including (but not necessarily) up until the end of the program. (link)

mdeloof commented 1 year ago

Hi, just wanted to check back with you if you managed to solve your problem?

(I probably shouldn't have closed this issue before given you a chance to respond, sorry for that).

k3d3 commented 1 year ago

Oh, no worries! That did solve my issue, thank you :)