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 ReadyFail and also from FailReady. 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, enums 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.↩︎