A state function signature

Hi there. I'm looking for a way to convey:

pub fn event_handler(state: &State, event: Event) -> Option<State>

more like:

pub fn event_handler(state: &State, event: Event) -> State

...which feels more natural... Thing is, the above can't be done as I can't copy State to return it if it hasn't changed. I also want to avoid passing the existing State in by value.

The event_handler conditionally yields a new State. Looking at the Cow type seems as though it may be the go... but it isn't available for nostd which is my world.

Any advice? Thanks.

1 Like

I don't understand why this would be true.

1 Like

If you at least have alloc, you can use Cow from here:

1 Like

You can create your own Cow-like enum without the clone part.

1 Like

Can you elaborate what feels unnatural about it? You could always just make a new enum like

enum Transition {
    ToOther(State),
    StayAtSelf,
}

(Self is a keyword, so I couldn't just use that as the name for the variant representing a self transition.)

1 Like

Perhaps not ironically, a Transition enum looks just like Option:

enum Transition{
    Some(State),
    None
}
1 Like

Because my State has no Copy trait... So to return it, I have to create a new one that I own. At least that's how I understand it. :slight_smile:

Ah ha! I didn't find that before. Thanks!

Thanks. By feeling "more natural", I probably mean, "what I'm used to!". In the JVM world, I'd pass in a state object reference and have returned a state object reference. Thus, the second signature I provided looks more like what I'd expect. That said, what I expect doesn't mean idiomatic Rust at this stage (I'm a newbie to Rust).

Here's what I came up by using the Cow type, revealing some of the implementation too:

pub fn event_handler(state: &State, event: Event) -> Cow<State> {
    match *state {
        State::Idle { ref observations } => match event {
            Event::EntryExitsEmitted => Cow::Borrowed(state),
            Event::ExpectingInsideMovement => Cow::Owned(State::PendingInsideMovement {
                observations: observations.clone(),
            }),
...
        },
...
}

My calling code looks like this:

let mut new_state = prev_state;
for event in events.iter() {
    new_state = event_handler(&new_state, *event).into_owned();
}

In my particular use-case, the events supplied will have a prev_state that's valid and cause Cow::Owned types to be returned i.e. into_owned should never cause a clone to occur in reality.

Thanks for the feedback so far. Happy to take further advice being the newbie that I am.

"Reference" in the JVM world (or, more generally, in most garbage-collected languages) is more like Rc<RefCell<T>> in Rust (or Arc<Mutex<T>>, is they are thread-safe), since they are a kind of "shared-mutable-owned". You can use this everywhere, if you like, but I'm afraid this will quickly make your code very hard to understand.

3 Likes

Sure thing - not trying to reproduce the JVM world here. Thanks.

You could try to make your state cheap to clone by wrapping its non-copyable fields in Arc.

2 Likes

Thanks. This is what I’ve done, only I’m using Rc, not Arc. My context is a single threaded one for an embedded project.

I think this is a good opportunity to look at what you're doing here from an ownership perspective.

You're passing a borrow of new_state to the event_handler, but then you just don't use it any more. So from how it's used, it seems like it'd be perfectly reasonable to pass it in owned. Then there's something inherently nice about

let mut new_state = prev_state;
for event in events.iter() {
    new_state = event_handler(new_state, *event);
}

That means there's definitely never a clone needed, and if there's any complex data in a state that doesn't need to be cloned internally (like the observations looks like it is).

(EDIT: could maybe even for event in events, and the *event could just be event. Dunno if you need the events elsewhere too.)

So the implementation might be more like this:

pub fn event_handler(state: State, event: Event) -> State { // EDIT: Oops, forgot to remove the `Cow` here
    match state {
        State::Idle { observations } => match event {
            Event::EntryExitsEmitted => state,
            Event::ExpectingInsideMovement => State::PendingInsideMovement {
                observations: observations,
            },
...
        },
...
}

Which seems shorter and more efficient.

So basically the solution to not being Copy is to just pass in something owned instead :slight_smile:

1 Like

Thanks. I've now created something a little more abstract in the playground. Hopefully, this helps: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8bc30b1d269b9544e64d9467465aae22. In particular, I don't understand how a Cow<State> can be returned in your example.

Also, I feel that I should be passing the state in by reference and not by value, as it is essentially a struct that may evolve over time to include more fields.

Perhaps you mean dropping the Cow altogether? https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91b0ca53cb49c710cf2fa154849b0c66

Thanks to @scottmcm, I've now re-thought ownership and conclude that I was overthinking the costs of Rust's move behaviour and wanting to pass references instead. In actuality, the moves will likely become optimised out by LLVM and, even if not, the moves are small. This is the final signature I arrived at:

pub fn event_handler(state: State, event: Event) -> State

...and here's a working example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=40074f6a61a84f28a036ea235a9be74f

1 Like

Oops, yes I did. I've fixed my post above.

I would propose something like

pub fn event_handler(state: State, event: Event) -> State {
    match (state, event) {
        (State::State0 { shared_data }, Event::Event1) => State::State1 { shared_data },
        (State::State1 { shared_data }, Event::Event2) => State::State0 { shared_data },
        
        // for everything else, just ignore the event and stay where you already are.
        (state, _) => state,
    }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9357d6be627ac8fdf3a8a3a6d47a131a

1 Like

Thanks again. I like to specify each match branch explicitly when writing state machine handlers as it then makes me stop and think a little more about what happens in each situation. It often pays off. :slight_smile: