Skip to content

Instantly share code, notes, and snippets.

@atierian
Created August 31, 2021 14:14
Show Gist options
  • Save atierian/0c4a41c2383572b30c2d7c30a0057f90 to your computer and use it in GitHub Desktop.
Save atierian/0c4a41c2383572b30c2d7c30a0057f90 to your computer and use it in GitHub Desktop.
Building State Machines in Swift
/*
Cory Benfield - Building State Machines in Swift
https://www.youtube.com/watch?v=7UC7OUdtY_Q
What is a Finite State Machine?
- Structured way to represent computation
- System can be in one of a finite number of states at any time
- Reacts to inputs by changing state, and optionally producing a side effect
- Deterministic and nondeterministic flavors
- Simple model of computation: easy to understand
------------
| No Pizza |
------------
|
Order Pizza
|
------------
| Ordered |
------------
/ \
Make Payment Receive Pizza
/ \
--------------------- ------------------
| Awaiting Delivery | | Payment Owed |
--------------------- ------------------
\ /
Receive Pizza Make Payment
\ /
-------------------------
| Pizza Ready |
-------------------------
Examples of State Machine Systems
- Shopping cart checkouts
- Traffic light management
- Regular expression parsers
- In fact, many many kinds of parsers
- Almost any medical device
- Combinations of other state machines
- Network protocols
Advantages
- Ability to be represented visually, helps understanding
- Readily debuggable
- Robust way to represent asynchronous or highly stateful computation
- Particularly valuable for distributed systems design
Examples of Distributed Systems
- Distributed Databases
- Web apps
- Internet connected native apps
- Non-distributed databases
- Non-internet connected native apps
- Basically all software
FSMs in Distributed Systems
- Structured tool for handling uncertainty
- Force you to enumerate in which states an event is acceptable
- Force you to decide what effect an unacceptable input has
- Encourage you to design for these behaviors
- Easily extend if you fail to design for these behaviors initially
Using Finite State Machines leads to understandable system state
Downsides
- Can be more verbose
- Often require massive switch statements
- Easy to accidentally "escape" state from the state machine
- Incomplete enumeration of states of inputs leads to subtle bugs
- Not a natural style of thought for most developers
Good state machine design restricts the ability to start with the incorrect state.
Scaling Up
- Works for pretty big state machines
- Battle-tested in swift-nio-http2
- Good performance characteristics
- High reliability, perfect for servers
- Clear control flow by returning values instead of making outcalls
Take Home Lessons
- Consider explicit state machines for managing stateful computation
- Think carefully about whether complex state could be simplified
- Compute synchronously on state and asynchronously with the network/user
- Simple tools work really well
*/
typealias Balance = Double
typealias Price = Double
struct PizzaStateMachine {
private var state: State
enum State {
case noPizza
case ordered(OrderedState)
case awaitingDelivery
case paymentOwed(PaymentOwedState)
case pizzaReady
}
struct OrderedState {
var pendingBalance: Balance
}
struct PaymentOwedState {
var pendingBalance: Balance
init(_ previousState: OrderedState) {
pendingBalance = previousState.pendingBalance
}
}
init() {
state = .noPizza
}
mutating func orderPizza() throws {
switch state {
case .noPizza:
state = .ordered(.init(pendingBalance: .init()))
case .ordered, .paymentOwed, .awaitingDelivery, .pizzaReady:
throw PizzaError.invalidTransition(state: state, event: "orderPizza")
}
}
mutating func makePayment(_ payment: Price) throws {
switch state {
case .ordered(var state):
state.makePayment(payment)
case .paymentOwed(var state):
state.makePayment(payment)
case .noPizza, .awaitingDelivery, .pizzaReady:
throw PizzaError.invalidTransition(state: state, event: "makePayment")
}
}
}
extension PizzaStateMachine.OrderedState: MakePaymentState {
var paymentCompleteState: PizzaStateMachine.State {
.awaitingDelivery
}
var paymentIncompleteState: PizzaStateMachine.State {
.ordered(self)
}
}
extension PizzaStateMachine.PaymentOwedState: MakePaymentState {
var paymentCompleteState: PizzaStateMachine.State {
.noPizza
}
var paymentIncompleteState: PizzaStateMachine.State {
.paymentOwed(self)
}
}
protocol MakePaymentState {
var paymentCompleteState: PizzaStateMachine.State { get }
var paymentIncompleteState: PizzaStateMachine.State { get }
var pendingBalance: Balance { get set }
}
extension MakePaymentState {
mutating func makePayment(_ payment: Price) -> PizzaStateMachine.State {
self.pendingBalance -= payment
if self.pendingBalance <= 0 {
return self.paymentCompleteState
} else {
return self.paymentIncompleteState
}
}
}
enum PizzaError: Error {
case invalidTransition(state: PizzaStateMachine.State, event: String)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment