I've got yet-another-new-approach for the event model. I originally went with a set of hierarchical enum
s (so MachineEvent::Allocator
contained an AllocatorEvent
, and then AllocatorEvent::RxAllocated
was one variant of the latter). This allowed the top-level Core to dispatch based on the machine name, and then the individual machine processing functions do type-checking for us: Allocator.process(&mut self, event: AllocatorEvent)
.
But the return value could be any sort of MachineEvent
(or API events, or IO events), and so I needed yet another layer of enums on top of MachineEvent
(which I called ProcessEvent
), and the return signature of the dispatch function was process(..) -> Vec<ProcessEvent>
, and every clause in the dispatch function had to build a gigantic tower of return values to make it match that signature (ProcessEvent::Machine(MachineEvent::Allocator(AllocatorEvent::RxAllocated))
).
So I switched to a big flat table of events (all events, even IO and API), using the machine name as a one-letter prefix, with names like A_RxAllocated
. I added an enum of machine names, and a function that mapped event variant to machine. All the processing functions accepted and received the same Event type. But then the compiler thinks that the processing functions might be given an event that's really always dispatched to someone else (e.g. because A_RxAllocated
is in Event
, the Send machine has to prepare for receiving it too, even though it's always going to be sent to Allocator). So those process()
functions got a lot of uncomfortable _ => panic!()
clauses.
But.. now I've learned slightly more about generics and the From
/Into
traits, and I think I have a way to go back to the nested enums. Basically it's creating a new type (named Events
) that behaves a lot like Vec
(or maybe VecDeque
), except that the .push()
method takes anything that can be converted into a MachineEvent
:
impl Events {
fn push<T>(&mut self, item: T) where MachineEvent: std::convert::From<T> {
self.events.push(MachineEvent::from(item));
}
}
The processing functions accept machine-specific types (Allocator
takes an AllocatorEvent
), but they all return an Events
. And when they add things to the Events
, they can give it specific event objects instead of wrapping them in something first:
let mut events = Events::new();
events.push(Send::GotVerifiedKey(key));
I experimented for a while with having Events
be a vector of "objects with trait X", where X meant that you could convert it into a MachineEvent if necessary, but I got stuck trying to deal with the Size issues. Eventually I decided that giving all processing functions the same return signature was important, and that meant the conversion into the common type has to happen as quickly as possible. Having a special vector-like thing which does the conversion on input seemed to do the trick.
I was able to copy the vec!
macro from the Rust Book into the file, which makes returning multiple events easier:
// Rendezvous::ConnectionMade
match self.state {
Connecting => events![Nameplate::Connected, Allocator::Connected, Lister::Connected]
}
Having that macro is sufficient. Which is good, because I don't think I'd be able to implement .extend()
or any of the Vec methods that take multiple items at the same time, since the monomorphization is only going to work with one type at a time. And requiring the processing functions to build up the return vector one item at a time would be a drag.
I'm working on rewriting all the stubs to work this way now, so maybe don't put too much time into porting any of the other machines until that's done.