Awesome
HSM
UML statecharts (Hierarchical State Machine) framework implementation in Swift (Disclaimer: the framework is under development and is subject to change.)
<p> <img src="https://img.shields.io/badge/Swift-5.1-orange" alt="Swift" /> <img src="https://img.shields.io/badge/platform-macOS%20|%20iOS-orange.svg" alt="Platform" /> <img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-orange" alt="SPM" /> <a href="https://github.com/SerhiyButz/HSM/blob/master/LICENSE"> <img src="https://img.shields.io/badge/licence-MIT-orange" alt="License" /> </a> </p>TODO: Work through the idea of using the Actor approach like in the ImageDownloader example from [https://developer.apple.com/videos/play/wwdc2021/10133] to address the run to completion problem when state transition in asynchronous/concurrent code. For example, the need for transient states "inProgress"/"is...ing" involves some overhead strategy logic, for example, to discard concurrent controls of flow when they meet the state machine in those states. Another approach whould be to use completion handlers for all calls to the state machine, that are automatically called back when the state is back to normal (non-transient).
TODO: My HSM doesn’t address the problem of state scope (lifetime). When transitioning from a state, its attributes (dependencies) should be destroyed, and on transitioning to a state, its attributes (dependencies) should be created anew.
Contents
Intro
User Interface (UI) - provides the interaction between people and machines. A good user interface is simple, efficient, enjoyable, ergonomic, and intuitive (user-friendly).
The first UI in which people interacted with a computer system in real time was the command line interface (CLI). It was a simple UI in terms of implementation, where the user had to type commands that the system would execute. In order to interact with the system, the user had to know the command language. And users were responsible for making sure that the interaction syntax was always correct.
With the development of computer graphics, another class of UI's emerged: Direct Manipulation Graphical User Interfaces (GUI's), which are much easier to use than CLI, because the user does not need to know the command language to interact with the system. Instead, individual graphical elements such as buttons, scrollbars, and windows represent different program entities, and the user can directly interact with these entities without having to type commands. This UI allows the user to interact effectively with the system without any prior knowledge of the interface. UI's with direct manipulation are easy to use because the state of the object is visible to the user and the user can manipulate the object directly.
GUI software is built on top of a reactive application framework (a supervisory event-driven infrastructure) with business logic actions performed asynchronously in event handler callbacks. Control resides in the event-driven infrastructure, so from the application standpoint the control is inverted compared to a traditional sequential program (as in the case of the old batch-interface software).
Each GUI object can respond to external events, such as those coming from the user (as part of the user interaction), the operating system, or the application itself. The sequence of events entering the application determines the control flow. The developers are responsible for ensuring the syntax of the control flow (including the user interaction) is always correct.
The correct (expected) control flow consists of valid event orderings or "happy paths". The problem is, as the number of possible events increases, the number of "unhappy paths" (unexpected orderings of events) has a factorial growth and it quickly outgrows the capacity of the human mind, and there always comes a point where bugs are inevitable.
UI developers face the challenge of ensuring that all UI objects and dialogs in the application are coordinated in such a way that it is impossible for a user to perform operations that would lead to an error. UI objects do not behave independently of each other and the developer is responsible for ensuring that a user can only supply valid events at any given time. In fact, the events a user supplies (when interacting with the UI) cause the application to move from one set of possible events to another. In other words, the UI moves from one state to another, and the state defines the set of possible events a user can supply. The states define the context in which an event occurs. Mostly, the states are not explicitly specified in the UI code. However, if these states were to be represented on a diagram explicitly and used as the basis for constructing the control layer objects, the UI would be much easier to build.
This is where the event-state-action paradigm (as opposed to the oversimplified event-action paradigm) comes in, leading to such a technique as Finite State Machine (FSM). But for a long time, the idea of building a real UI-based project on FSM was not considered viable because of the problem known as the state explosion problem (large number of states and event arrows) that occurs when using state transition diagrams as the design notation for designing any but trivial UI's. This problem was later addressed by UML statechart notation (originally proposed by David Harel), which is essentially a hierarchical extention to the FSM, Hierarchical State Machine (HSM).
Here are some facts about UML statecharts:
- The use of UML statecharts naturally leads to the use of a top-down approach to behavior modeling, where the most general/abstract behavior is first introduced, and then the subtleties of that behavior are refined through programming by difference. This is also the key to the long-term maintainability of such software.
- The power of UML statecharts is based on the reuse of behavior. The same idea is used in Ultimate Hook pattern, which is common in GUI's, or Chain of Responsibility Pattern. The active sub-state, which is at a lower level of the state hierarchy, has the ability to respond first to each event; thus it can choose to react in any way it likes. At the same time, all unhandled events bubble up to the higher level, where they are processed according to the more general behaviors of the super-states. This is an example of programming by difference because the UI programmer needs to code only the differences from the more general behaviors.
- UML statecharts have been invented as “a visual formalism for complex systems”, so from their inception, they have been inseparably associated with graphical representation in the form of state diagrams. The best way to capture the precise behaviour of a UI is to produce a model of the UI behaviour in a graphical language that has well-defined semantics. In addition, UML statecharts are an excellent means of communication between technical and non-technical members of the project team.
- UML statecharts technology was fundamentally used in Nasa's Deep Space 1 (DS1) Mission and Mars Science Lab (MSL) Mission. For example, code automatically generated from a state machine diagram has been part of Curiosity’s flight software since launch, and continues to run onboard today.
This framework was developed as part of a exploration of different architectures for structured behavior modeling in the development of the UI control layer in Swift.
Features:
-
UML standard compliance was in mind (but not yet reached full complienсe)
-
Hierarchical states with full support for behavioral inheritance
-
Orthogonal regions
-
State entry and exit actions for initialization and cleanup and also transition actions
-
Internal and external self-transitions
-
Initial, fork and join pseudostates
-
History mechanism (both shallow and deep history)
-
Actor (active object) model
-
Run-To-Completion model
-
Extended state support, and etc
The framework was developed with the following aspirations:
- It should be simple to use and maintain. Defining state should be as easy as defining OOP classes.
- It should allow for easy changes in the state machine topology (state nesting and state transitions). No manual coding of transition chains should be required.
- It should provide good runtime efficiency and take up little memory. The cost of dispatching events in a state machine should be comparable to calling virtual functions in OOP.
- It should be as UML-compliant as possible.
- The verbosity of the code for using the state machine should be reduced as much as possible.
Note: These requirements often contradict each other, so this implementation strives to achieve a balanced implementation.
Further development objectives:
- Improving syntax and modernizing the API
- Achieving full automatic scoping of the extended state
- Implementing composite state's encapsulation (by means of entry/exit point)
Usage
An understanding of Harel's statecharts and their concepts is required for work with this framework. At the end of this page you will find some links to resources on this topic.
Consider the following statechart diagram:
You can represent this diagram in the framework with the following code:
class MyTop: TopState<MyEvent> {
class Substate1: State<MyTop, MyTop> {
override func entry() { ... } // optional
override func exit() { ... } // optional
override func handle(_ event: MyEvent) -> Transition? { // optional
switch event {
case .evt1(let flag) where flag:
return Transition(to: superior.substate2)
default:
return nil
}
}
}
let substate1 = Substate1()
class Substate2: State<MyTop, MyTop> {
override func entry() { ... } // optional
override func exit() { ... } // optional
override func handle(_ event: MyEvent) -> Transition? { ... } // optional
}
let substate2 = Substate2()
override func initialize() {
bind(substate1, substate2)
initial = substate1 // optional
historyMode = .shallow // optional
}
}
Here we have a top state MyTop
which is a composite state and it's parameterized by our event type MyEvent
. The MyTop
contains 2 sub-states Substate1
and Substate2
. All sub-states in the framework have the same structure. A substate must be explicitly parameterized with two types: (1) its immediate superior state, the immediate container (or parent), (2) and the top state (which is always the most superior state). This serves to bind all state types nested in the hierarchy of states, and to enable statically referring to the immediate superior and topmost state from event handlers and state reactions; for this each state has 2 properties superior
and top
.
Each state must bind its sub-states in the initialize()
method, passing them as arguments in the bind(...)
call. The initialize()
method is also the method in which you configure initial (or default) sub-state as well as the state history mode.
Demos
Here are simple demo apps so that you can try and hands-on experiment with the framework yourself.
HSM-based running lights demo
Statechart diagram:
Here's the source code of the HSM-based application controller for the above statechart diagram:
import Foundation
import HSM
/// HSM-based controller
class Controller: TopState<Event> {
// MARK: - Substates
class Lights: State<Controller, Controller> {
class Red: State<Lights, Controller> {
override func entry() {
top.actions.turnOnRedLed()
}
override func exit() {
top.actions.turnOffRedLed()
}
override func handle(_ event: Event) -> Transition? {
switch event {
case .timerTick: return Transition(to: superior.green)
default: return nil
}
}
}
let red = Red()
class Green: State<Lights, Controller> {
override func entry() {
top.actions.turnOnGreenLed()
}
override func exit() {
top.actions.turnOffGreenLed()
}
override func handle(_ event: Event) -> Transition? {
switch event {
case .timerTick: return Transition(to: superior.blue)
default: return nil
}
}
}
let green = Green()
class Blue: State<Lights, Controller> {
override func entry() {
top.actions.turnOnBlueLed()
}
override func exit() {
top.actions.turnOffBlueLed()
}
override func handle(_ event: Event) -> Transition? {
switch event {
case .timerTick: return Transition(to: superior.red)
default: return nil
}
}
}
let blue = Blue()
// MARK: - Initialization
override func initialize() {
bind(red, green, blue)
initial = red
historyMode = .shallow
}
// MARK: - Lifecycle
override func entry() {
top.actions.clear()
}
override func handle(_ event: Event) -> Transition? {
switch event {
case .buttonTap: return Transition(to: superior.paused)
default: return nil
}
}
}
let lights = Lights()
class Paused: State<Controller, Controller> {
override func handle(_ event: Event) -> Transition? {
switch event {
case .buttonTap: return Transition(to: superior.lights)
default: return nil
}
}
}
let paused = Paused()
// MARK: - Properties
let actions: Actions
// MARK: - Initialization
init(actions: Actions) {
self.actions = actions
}
override func initialize() {
bind(lights, paused)
initial = lights
}
}
HSM-based calculator demo
Statechart diagram:
Examine the source code of the HSM-based application controller for the above statechart diagram here.
Installation
Swift Package as dependency in Xcode 11+
- Go to "File" -> "Swift Packages" -> "Add Package Dependency"
- Paste HSM repository URL into the search field:
https://github.com/SerhiyButz/HSM.git
- Click "Next"
- Ensure that the "Rules" field is set to something like this: "Version: Up To Next Minor: 0.11.1"
- Click "Next" to finish
For more info, check out here.
License
This project is licensed under the MIT license.
Resources
- Awesome Finite State Machines
- David Harel - Statecharts: A Visual Formalism for Complex Systems
- David Harel - Statecharts in the Making: A Personal Account
- Ian Horrocks - Constructing the User Interface with Statecharts
- Miro Samek - Practical UML Statecharts in C/C++: Event-Driven Programming for Embedded Systems
- OMG - Unified Modeling Language (UML)
- Statecharts
- The Statechart Perspective
- UML Statechart Autocoding for the Mars Science Lab (MSL) Mission
- Wiki - UML State machine