Skip to content

Instantly share code, notes, and snippets.

@ctreffs
Created May 19, 2022 09:46
Show Gist options
  • Save ctreffs/86e1aeaffdf0e26f6c3ff8e6ae552b5d to your computer and use it in GitHub Desktop.
Save ctreffs/86e1aeaffdf0e26f6c3ff8e6ae552b5d to your computer and use it in GitHub Desktop.
FSM
//
// StateMachine.swift
//
//
// Created by Christian Treffs on 24.06.20.
//
public typealias FSM<State, StateInstance> = StateMachine<State, StateInstance> where State: Equatable
public final class StateMachine<State, StateInstance> where State: Equatable {
/// Current state of this finite-state machine.
public private(set) lazy var current: State = { fatalError("no current state set!") }()
private lazy var lazyCurrentInstance: StateInstance? = { fatalError("no current instance set!") }()
private var actionsOnEnter: [TransitionAction<State>]
private var actionsOnExit: [TransitionAction<State>]
private var stateInstances: [StateId: StateInstanceBox<State, StateInstance>]
private init() {
actionsOnEnter = []
actionsOnExit = []
stateInstances = [:]
}
private var subcriptionsId: UInt = 0
private func nextSubscriptionId() -> UInt {
defer { subcriptionsId += 1 }
return subcriptionsId
}
@discardableResult
public func onEnterState(_ state: State, execute closure: @escaping () -> Void) -> TransitionActionSubscription {
let subscription = TransitionActionSubscription(id: nextSubscriptionId(), isOnEnter: true)
let action = TransitionAction(id: subscription.id, source: nil, destination: state, closure: closure)
actionsOnEnter.append(action)
return subscription
}
@discardableResult
public func onEnterState(_ state: State, from sourceState: State, execute closure: @escaping () -> Void) -> TransitionActionSubscription {
let subscription = TransitionActionSubscription(id: nextSubscriptionId(), isOnEnter: true)
let action = TransitionAction(id: subscription.id, source: sourceState, destination: state, closure: closure)
actionsOnEnter.append(action)
return subscription
}
@discardableResult
public func onExitState(_ state: State, execute closure: @escaping () -> Void) -> TransitionActionSubscription {
let subscription = TransitionActionSubscription(id: nextSubscriptionId(), isOnEnter: false)
let action = TransitionAction(id: subscription.id, source: state, destination: nil, closure: closure)
actionsOnExit.append(action)
return subscription
}
@discardableResult
public func onExitState(_ state: State, to destinationState: State, execute closure: @escaping () -> Void) -> TransitionActionSubscription {
let subscription = TransitionActionSubscription(id: nextSubscriptionId(), isOnEnter: false)
let action = TransitionAction(id: subscription.id, source: state, destination: destinationState, closure: closure)
actionsOnExit.append(action)
return subscription
}
@discardableResult
public func unsubscribe(_ subscription: TransitionActionSubscription) -> Bool {
switch subscription.isOnEnter {
case true:
if let index = actionsOnEnter.firstIndex(where: { $0.id == subscription.id }) {
actionsOnEnter.swapAt(index, actionsOnEnter.endIndex - 1)
actionsOnEnter.removeLast()
return true
}
case false:
if let index = actionsOnExit.firstIndex(where: { $0.id == subscription.id }) {
actionsOnExit.swapAt(index, actionsOnExit.endIndex - 1)
actionsOnExit.removeLast()
return true
}
}
return false
}
private func shouldPerformTransition(_ transition: Transition<State>) -> Bool {
transition.isTransient
}
}
extension StateMachine: Equatable where State: Equatable {
public static func == (lhs: StateMachine<State, StateInstance>, rhs: StateMachine<State, StateInstance>) -> Bool {
lhs.current == rhs.current
}
}
extension StateMachine where StateInstance == Void {
public convenience init(initial initialState: State) {
self.init()
current = initialState
}
}
extension StateMachine where State: Hashable {
public var currentInstance: StateInstance? {
self.lazyCurrentInstance
}
public convenience init(initial initialState: State, initialInstance: @autoclosure @escaping () -> StateInstance) {
self.init()
addInstance(for: initialState, instance: initialInstance())
performTransition(Transition(from: nil, to: initialState))
}
@discardableResult
public func addInstance(for state: State, instance: @autoclosure @escaping () -> StateInstance) -> Bool {
let stateId = StateId.make(from: state)
guard stateInstances[stateId] == nil else {
return false
}
let box = StateInstanceBox<State, StateInstance> { [weak self] in
let newInstance = instance()
if let stateObserving = newInstance as? StateChangeObserving {
self?.onEnterState(state, execute: stateObserving.onEnter)
self?.onExitState(state, execute: stateObserving.onExit)
}
return newInstance
}
stateInstances[stateId] = box
return true
}
@discardableResult
public func transition(to newState: State) -> Bool {
let transition = Transition(from: current, to: newState)
guard shouldPerformTransition(transition) else {
return false
}
performTransition(transition)
return true
}
private func performTransition(_ transition: Transition<State>) {
let newState = transition.destination
let newInstance = getInstance(for: newState)
performExit(transition)
current = newState
lazyCurrentInstance = newInstance
performEnter(transition)
}
private func getInstance(for state: State) -> StateInstance? {
stateInstances[StateId.make(from: state)]?.instance
}
/// an entry action: performed when entering the state, and
private func performEnter(_ transition: Transition<State>) {
actionsOnEnter
.forEach { if $0.isApplicable(on: transition) { $0.closure() } }
}
/// an exit action: performed when exiting the state.
private func performExit(_ transition: Transition<State>) {
actionsOnExit
.forEach { if $0.isApplicable(on: transition) { $0.closure() } }
}
}
public struct Transition<State> where State: Equatable {
public let source: State?
public let destination: State
public init(from source: State?, to destination: State) {
self.source = source
self.destination = destination
}
public var isTransient: Bool {
source != destination
}
}
extension Transition: Equatable where State: Equatable { }
public struct TransitionActionSubscription {
let id: UInt
let isOnEnter: Bool
}
private struct TransitionAction<State: Equatable> {
let id: UInt
let source: State?
let destination: State?
let closure: () -> Void
func isApplicable(on transition: Transition<State>) -> Bool {
switch (source, destination) {
case let (.some(source), .some(destination)):
return transition.source == source &&
transition.destination == destination
case let (.some(source), .none) :
return transition.source == source
case let (.none, .some(destination)):
return transition.destination == destination
case (.none, .none):
return true
}
}
}
private enum StateId: Equatable, Hashable {
case hashValue(Int)
static func make<State>(from state: State) -> StateId where State: Hashable {
.hashValue(state.hashValue)
}
}
private class StateInstanceBox<State, StateInstance> {
let constructor: () -> StateInstance
lazy var instance: StateInstance = constructor()
init(_ stateConstructor: @escaping () -> StateInstance) {
constructor = stateConstructor
}
}
public protocol StateChangeObserving {
func onEnter()
func onExit()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment