I'm new to Swift, and as a learning exercise, I'm trying to make a framework for a simple state machine. But I haven't been able to find the right combination of protocols/inheritance/generics/opaque types to achieve what I want.
The overall goal is to have:
- various
State
structs, which hold relevant data for that state. - various
Event
structs. - each
State
has zero or morehandle(event:)
functions for specific event types, which return a newState
object (representing the state after processing that event).
I'm hoping that using this might look like:
struct Start: Event {}
struct Stop: Event {}
struct ReceivedConnection: Event {
let connection: Connection
}
struct Off: State {
func handle(event: Start) -> Listening {
return Listening()
}
}
struct Listening: State {
func handle(event: ReceivedConnection) -> Connected {
event.connection.start()
return Connected(connection: event.connection)
}
func handle(event: Stop) -> Off {
return Off()
}
}
struct Connected: State {
let connection: Connection
func handle(event: Stop) -> Off {
connection.stop()
return Off()
}
}
As you can see, I'm basically hoping to use the type system to model:
- What state transitions are valid
- When it's valid to receive a kind of event
- What auxiliary data exists for each state
But I can't figure out how to implement a State
that actually makes this work. I've tried inheritance, protocols using generics, and protocols using some
/any
. (Note that I used struct
s above for the example, but I don't care whether it uses value or reference types.)
I want to write something like (this does not work, for multiple reasons):
protocol State {
func handle(event: some Event) -> any State
// ^ Intended meaning: given a _concrete_ event, return something that's a `State`.
// This does *not* mean that, to be a `State`, you must be able to handle *any* type of `Event`.
// (Whether the return type is opaque, or boxed, or something else, isn't important to me.)
}
struct StateMachine {
var state: any State
mutating func handle(event: some Event) throws {
// TODO: what happens when `event` is invalid for `state`?
// How do we recognize this and throw an error?
state = state.handle(event: event)
}
}
The concrete State
s don't conform to this protocol, because they don't take an opaque some Event
, but one concrete Event
type.
I think what I want is basically dynamic dispatch: pick the matching method based on the concrete types at runtime. Additionally, I want to allow and handle the possibility that no methods match at runtime: this was an invalid Event
, so we should throw an error.
My understanding of How to do dynamic dispatch? - #6 by anthonylatsis is that this isn't really possible in Swift. But maybe I'm missing something?
And if this isn't possible, any suggestions for a more idiomatic way to represent this?