Last active
September 26, 2022 15:59
-
-
Save monyschuk/e2c5609599195a30cc66 to your computer and use it in GitHub Desktop.
Simple Swift FSM
This file contains 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
//: Playground - noun: a place where people can play | |
import UIKit | |
/// A finite state machine | |
final class FSM<State, Transition>: ObservableObject where State: Hashable, Transition: Hashable { | |
/// current state | |
@Published private(set) var state: State | |
/// state transition graph | |
private(set) var transitions = [State:[Transition:State]]() | |
/// Adds a transition `transition` from state `first` to state `second` | |
/// - Parameters: | |
/// - transition: a transition | |
/// - first: the starting state | |
/// - second: the ending state | |
func addTransition(_ transition: Transition, from first: State, to second: State) { | |
transitions[first, default: [:]][transition] = second | |
} | |
/// Returns `true` if `transition` will advance state | |
/// - Parameter transition: a transition | |
/// - Returns: `true` if the transition will advance state | |
func canAdvance(_ transition: Transition) -> Bool { | |
return transitions[state]?[transition] != nil | |
} | |
/// a transition observation function | |
typealias Observer = (_ old: State, _ new: State) -> () | |
/// Advances state by performing `transition`. | |
/// - Parameters: | |
/// - transition: a transition | |
/// - observe: an optional transition observer | |
/// - Returns: the new state | |
@discardableResult | |
func transition(_ transition: Transition, observe: Observer? = nil) -> State { | |
let prev = state | |
if let next = transitions[prev]?[transition], next != prev { | |
state = next | |
observe?(prev, next) | |
} | |
return state | |
} | |
/// a transition builder function, used to add transitions to a newly created state machine | |
typealias TransitionBuilder = (FSM<State,Transition>)->() | |
/// Initializes the state machine with state `state` then optionally executes `builder` where | |
/// transitions between states can be configured on the receiver. If `builder` is nil, then transitions | |
/// can be added to the state machine after initialization | |
/// - Parameters: | |
/// - state: a starting state | |
/// - builder: a transition builder | |
init(state: State, transitions builder: TransitionBuilder? = nil) { | |
self.state = state | |
builder?(self) | |
} | |
fileprivate init(state: State, transitions: [State:[Transition:State]]) { | |
self.state = state | |
self.transitions = transitions | |
} | |
} | |
extension FSM: Equatable { | |
static func ==(lhs: FSM<State,Transition>, rhs: FSM<State,Transition>) -> Bool { | |
return lhs === rhs || lhs.state == rhs.state && lhs.transitions == rhs.transitions | |
} | |
} | |
extension FSM: Codable where State: Codable, Transition: Codable { | |
enum CodingKeys: String, CodingKey { | |
case state | |
case transitions | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.container(keyedBy: CodingKeys.self) | |
try container.encode(state, forKey: .state) | |
try container.encode(transitions, forKey: .transitions) | |
} | |
convenience init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let state = try container.decode(State.self, forKey: .state) | |
let transitions = try container.decode([State:[Transition:State]].self, forKey: .transitions) | |
self.init(state: state, transitions: transitions) | |
} | |
} | |
// | |
// Sample Usage | |
// | |
enum Switch { | |
case on, off | |
} | |
enum Position { | |
case up, down | |
} | |
var lights = FSM<Switch, Position>(state: .off) { | |
fsm in | |
fsm.addTransition(.up, from: .off, to: .on) | |
fsm.addTransition(.down, from: .on, to: .off) | |
} | |
// you can observe state changes, since FSM is an `ObservableObject` | |
let obs = lights.$state.sink(receiveValue: { | |
print($0) | |
}) | |
// sending transitions to the FSM causes its state to change for defined state changes | |
lights.transition(.up) | |
lights.transition(.down) | |
lights.transition(.down) | |
lights.transition(.up) |
How about splitting advance and the actions performed? I assume that in a non trivial system, different parts of the system will want to perform different actions depending on what transition was observed. So you should be able to register multiple observers for for a transition.
I don't mean this as criticism, but rather asking your opinion to see if I have understood the intended usage of this FSM correctly. I assume you have done your current design because it makes the working of it simpler and easier to demonstrate.
It's been a while, but Swift now makes this easy to do without bloating the code, so I've updated the gist to make StateMachine
(now FSM
) an ObservableObject
with an @Published
state.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Looks like enums with associated values either can now, or will shortly able to be made hashable automatically via the compiler.