Created
August 31, 2021 14:14
-
-
Save atierian/0c4a41c2383572b30c2d7c30a0057f90 to your computer and use it in GitHub Desktop.
Building State Machines in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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