Swift State Machines, Part 4: Redirect
In Part 3 of our talk about state machines, we discussed the importance of keeping the parts that specify transitions separate from the parts that specify behavior. We’ve done a pretty good job, so far! But there’s still a common case we hit too often:
enum State:StateMachineDataSource{
case Ready, Fail
func shouldTransitionFrom(from:State, to: State) -> Bool{
switch (from, to){
case (.Ready, .Fail), (.Fail, .Ready):
return true
default:
return false
}
}
}
func didTransitionFrom(from:State, to:State){
switch (from, to){
case (.Ready, .Fail(let error)):
processError(error)
myStateMachine.state = .Ready
}
}
Here we define a simple two-state machine1. We set up transitions from Ready → Fail and also from Fail → Ready. Finally, we handle the Fail state by processing any errors before finally setting the state back to Ready.
And this last bit is where lies the rub. Because the fact that Fail should transition back to Ready when reached isn’t logic that belongs in the behavior of our class. It’s really part of rules surrounding the definition of our transitions.
What we might like to see is something like:
func shouldTransitionFrom(from:State, to: State) -> Bool{
switch (from, to){
case (.Ready, .Fail):
return .Ready //NOPE!
case (.Fail, .Ready):
return true
default:
return false
}
}
This, of course, won’t work. Because what we really want is to return one of three things: true
, false
, or a State
. But there’s no type that represents “either a boolean or some enumeration or something”. Or is there?
As we’ve said, enum
s in Swift are curious beasts. While fulfilling the role of simple enumerations in C/ObjC, and of sets in our state machines, they are also, strictly speaking, sum types — that is, things that represent values with one of a number of given types.
That sounds promising. Instead of returning a simple Bool
, what if we made shouldTransitionFrom
return an enumeration? It’ll represents three values: “Yes, we should transition”, “No, we shouldn’t transition”, and “Redirect to the the given state”. We’ll call it Should
:
enum Should{
case Continue, Abort, Redirect(State)
}
func shouldTransitionFrom(from:State, to: State) -> Should{
switch (from, to){
case (.Ready, .Fail):
return Should.Redirect(.Ready)
case (.Fail, .Ready):
return Should.Continue
default:
return Should.Abort
}
}
We’ll have to update the setter of our StateMachine
’s computed state
property just a bit:
switch _state.shouldTransitionFrom(_state, to:newValue){
case .Continue:
_state = newValue
case .Redirect(let redirectState):
_state = newValue
self.state = redirectState
case .Abort:
break
}
But overall, not too bad for all the flexibility we get. For now our shouldTransitionFrom
not only specifies what transitions are legal, but also that transitions from Ready to Fail automatically transition back to Ready again. Our didTransitionFrom
implementation only needs concern itself with what to do at each stop on the road, not where the road itself is heading.
And what a crazy road it’s been! We’ve come a long way from our original implementation. But I think we can be pretty proud of where we’ve ended up.
Here’s the gist. Use it in good health.
1: The code here only only covers the operative bits. See Part 2 for the full details.↩︎