Quantcast
Channel: Macoscope Blog » Maciej Konieczny
Viewing all articles
Browse latest Browse all 3

Introducing SwiftyStateMachine

$
0
0

I’m happy to announce that we have open-sourced SwiftyStateMachine, a Swift microframework for creating finite-state machines, designed for clarity and maintainability. Check it out on GitHub.

The key characteristics of our framework are type-safety (compile-time errors in case of mistakes) and the ability to create diagrams of the state machines’ structure (similar to the image above), both of which are supposed to help in creating a state machine and maintaining it in the future. In this post I’ll describe these and the more subtle aspects of the framework, explain why they are there, and create a sample state machine.

Before we get there, however, let’s start with a primer on state machines.

What are state machines and what are they good for?

Finite-state machines seem to be very well known in some programming circles, especially among compiler hackers and game developers, while others are either unfamiliar with the concept or they know it but never think of using it. Don’t fret if you are in the latter group.

State machines are a way of looking at a problem in terms of a fixed set of states and well-defined transitions between those states. You can be only in one state at a time and you move to other states in response to events, performing predefined transitions. When changing state, you execute code associated with a given transition.

State machines won’t solve every problem out there, but they are a tool worth having in your toolbox. I like them because in many cases they fit my mental model really well and can be represented graphically. In the diagram at the beginning of this article, we used the standard way of representing states as ellipses and transitions as arrows. When working on, for example, a complex interaction or an animation, I find it really helpful to point a finger at the diagram and think “I am in this state and from here I can do this and this”. Many problems can be modeled after this approach and some even apply it to thinking about entire applications.

You should consider using a state machine if the problem you’re trying to solve can be divided into a small number of distinct states and the entity you are creating responds to a series of inputs or events over time. The more complex the state-event-state connections are, the better off you will be using a state machine instead of, say, a few boolean properties.

If you would like to read more about state machines, I recommend the State chapter of Robert Nystrom’s book Game Programming Patterns. The author goes to great lengths to describe the pattern and provides an example with C++ implementation. The whole book is good, too!

Now, back to our library.

Design Goals

Over the past year, I have used a few different state machine libraries and they all had their problems. The biggest obstacle was the fact that I couldn’t tell how the machines looked like. None of the libraries could create diagrams, the code was not all that readable and if the number of states was higher than five, I couldn’t keep them all straight anyway, so I ended up sketching up diagrams on pieces of paper. The problem got even more pronounced if the state machine was mutable and some other piece of code could extend its structure.

In Objective-C, type-safety was an issue. It was possible to pass an event from a different machine, forget to pass a value associated with the event, or forget to handle it in the transition. After making such a mistake, you would only get to know about it after running the code and experiencing a bug or a crash.

Finally, performance and memory leaks were a problem. State machines were created separately for each instance of an object they were controlling and this meant every transition block had to be created numerous times, which was an issue (small, but still) in the performance-intensive part of an app. The bigger problem was that each such block had to be checked for creating retain cycles leading to memory leaks. (While we are on the subject, I recommend Robb Böhnke’s method of unit testing memory leaks.)

When designing SwiftyStateMachine, I decided to exploit the power of Swift to get rid of all these problems. In some cases it was a complete success — the compiler will force you to do the right thing there. In other situations it will do the same, provided you follow a convention. Continue reading to see how it works in practice.

Example

In this example we’re going to implement a state machine the diagram of which you’ve seen at the top of the article. By now you probably scrolled far enough that you can’t see it, so here it is again:

This version was generated from code written using our framework. We’ll get to that later, but first let’s say a few words of introduction about the example itself.

The state machine we are going to create is necessarily simpler than something you would create in real life, but it shows the “spirit” of the pattern and use cases it can be applied to. Our machine represents a screen in an application that displays a list of items that can be refreshed with new data from the web. The screen can be in three states: displaying a list, displaying a refreshing/downloading animation, or displaying an error message if the refresh operation failed.

The code for this example is available as a playground.

Let’s start with defining enums for states and events:

enum ExampleState {
    case List, Refreshing, Error
}

enum ExampleEvent {
    case Refresh, ShowList, ShowError, DismissError
}

Next we have to specify the state machine layout. In SwiftyStateMachine that means creating a schema. Schemas are immutable structs that can be used by many StateMachine instances. They indicate the initial state and describe transition logic, i.e. how states are connected via events and what code is executed during state transitions.

Schemas incorporate three generic types: State and Event, which we defined above, and Subject which represents an object associated with a state machine. To keep things simple we won’t use subject right now, so we’ll specify its type as Void:

let schema = StateMachineSchema<ExampleState, ExampleEvent, Void>(initialState: .List) { (state, event) in
    switch state {
        case .List: switch event {
            // We use `_` to ignore the subject.
            case .Refresh: return (.Refreshing, { _ in println("refresh") })

            // We use `nil` to ignore events.  Notice we don't use `default`.
            // This way compiler will warn us about handling a new case, if we
            // add it to the enum in the future.
            case .ShowList, .ShowError, .DismissError: return nil
        }

        case .Refreshing: switch event {
            case .Refresh: return nil
            case .ShowList: return (.List, { _ in println("show list") })
            case .ShowError: return (.Error, { _ in println("show error") })
            case .DismissError: return nil
        }

        case .Error: switch event {
            case .Refresh: return (.Refreshing, { _ in println("refresh") })
            case .ShowList: return nil
            case .ShowError: return nil
            case .DismissError: return (.List, { _ in println("dismiss error") })
        }
    }
}

You probably expected nested switch statements after defining two enums. When following this convention (and not providing a default: case), we get compile-time errors when we forget about a state or event.

To understand the above snippet, it’s helpful to look at the initializer’s signature:

init(initialState: State,
    transitionLogic: (State, Event) -> (State, (Subject -> ())?)?)

We specify transition logic as a block. It accepts two arguments: the current state and the event being handled. It returns an optional tuple of a new state and an optional transition block. When the tuple is nil, it indicates that there is no transition for a given state-event pair, i.e. a given event should be ignored in a given state. When the tuple is non-nil, it specifies the new state that the machine should transition to and a block that should be called after the transition. The transition block is optional. It gets passed a Subject object as an argument, which we ignored in this example by using _.

Now, let’s create a machine based on the schema and test it:

// we use () as subject because subject type is Void
var machine = StateMachine(schema: schema, subject: ())

machine.handleEvent(.DismissError)  // nothing happens
if machine.state == .List { println("list") }  // prints "list"

machine.handleEvent(.Refresh)  // prints "refresh"
if machine.state == .Refreshing { println("refreshing") }  // prints "refreshing"

Cool. We can also get notified about transitions by providing a didTransitionCallback block. It is called after a transition with three arguments: the state before the transition, the event causing the transition, and the state after the transition:

machine.didTransitionCallback = { (oldState, event, newState) in
    println("changed state!")
}

OK, what about the diagram? SwiftyStateMachine can create diagrams in the DOT graph description language. To create a diagram, we have to use GraphableStateMachineSchema which has the same initializer as the regular StateMachineSchema, but requires state and event types to conform to the DOTLabelable protocol. This protocol makes sure that all elements have nice readable labels and that they are present on the graph (there’s no way to automatically find all enum cases):

extension ExampleState: DOTLabelable {
    static var DOTLabelableItems: [ExampleState] {
        return [.List, .Refreshing, .Error]
    }

    var DOTLabel: String {
        switch self {
            case .List: return "List"
            case .Refreshing: return "Refreshing"
            case .Error: return "Error"
        }
    }
}

extension ExampleEvent: DOTLabelable {
    static var DOTLabelableItems: [ExampleEvent] {
        return [.Refresh, .ShowList, .ShowError, .DismissError]
    }

    var DOTLabel: String {
        switch self {
            case .Refresh: return "Refresh"
            case .ShowList: return "ShowList"
            case .ShowError: return "ShowError"
            case .DismissError: return "DismissError"
        }
    }
}

Notice that above code doesn’t use a switch statement in the DOTLabelableItems implementation. We won’t get a compiler error after adding a new case to an enum. To get some help from the compiler in such situations, we can use the following trick:

static var DOTLabelableItems: [ExampleState] {
    let items: [ExampleState] = [.List, .Refreshing, .Error]

    // Trick: switch on all cases and get an error if you miss any.
    // Copy and paste the following cases to the array above.
    for item in items {
        switch item {
            case .List, .Refreshing, .Error: break
        }
    }

    return items
}

When our types conform to DOTLabelable, we can define our structure as before, but this time using GraphableStateMachineSchema. Then we can print the diagram:

let schema = GraphableStateMachineSchema// ...
println(schema.DOTDigraph)

Which produces:

digraph {
    graph [rankdir=LR]

    0 [label="", shape=plaintext]
    0 -> 1 [label="START"]

    1 [label="List"]
    2 [label="Refreshing"]
    3 [label="Error"]

    1 -> 2 [label="Refresh"]
    2 -> 1 [label="ShowList"]
    2 -> 3 [label="ShowError"]
    3 -> 2 [label="Refresh"]
    3 -> 1 [label="DismissError"]
}

On iOS we can even have the graph file saved in the repo each time we run the app in the simulator:

schema.saveDOTDigraphIfRunningInSimulator(filepathRelativeToCurrentFile: "machine.dot")

DOT files can be viewed by a number of applications, including the free Graphviz. If you use Homebrew, you can install Graphviz with the following commands:

brew update
brew install graphviz --with-app
brew linkapps graphviz

Graphviz comes with a dot command which can be used to generate graph images without launching the GUI app:

dot -Tpng machine.dot > machine.png

This ends our example and the tour of SwiftyStateMachine’s API. Enjoy improving your code by explicitly defining distinct states and transitions between them! While you do so, please keep two things in mind:

1) Remember that Swift enums can have associated values — you can pass additional information with events or store data in states. For example, if you had a game with a heads-up display, you could do something like this:

enum HUDEvent {
    case TakeDamage(Double)
    // ...
}

// ...

machine.handleEvent(.TakeDamage(13.37))

2) If your subjects are reference types (classes), remember to use unowned to break reference cycles:

let schema = // ...

class MyViewController: UIViewController {
    lazy var stateMachine = { [unowned self] in
    StateMachine(schema: schema, subject: self)
    }()

    // ...
}

Summary

State machines are a good solution to some specific problems you may encounter. You should consider using them if the problem you’re trying to solve can be divided into a small number of distinct states and the entity you are creating responds to a series of inputs or events over time.

Swift helps create APIs that are type-safe, expressive, and readable. Our framework, SwiftyStateMachine, is designed to use features of Swift to resolve issues we faced when using other libraries:

  • enums provide us with compile-time errors when we forget to handle a state or event
  • generics protect us from passing events from other state machines
  • reusable schemas improve performance and make detecting retain cycles easier by binding with a subject in only one place

On top of this, SwiftyStateMachine can create graphs. I encourage you to take it for a spin.

Happy coding!


Viewing all articles
Browse latest Browse all 3

Trending Articles