In 1987 Dr. David Harel introduced a visual modeling technique for defining system behavior that he called Statecharts (see as an example the Citizen watch model above). His notation was subsequently adopted by Rational Software as part of its UML specification in the mid-90s, lending the approach a broad audience of software architects and engineers.
In his paper, Dr. Harel described the notation as a “Visual Formalism for Complex Systems” which extended traditional notation for modeling automata with some powerful additional concepts. Key among these were the ideas of states having enter and exit events for initialization and cleanup, hierarchical state machines, state histories and orthogonal machines running in parallel.
As a professional developer interested in software architecture, over the years I developed a standardized pattern for implementing Statecharts in a number of different object-oriented languages. Subsequently I began to develop a shorthand notation for defining these patterns that has now evolved into the Frame System Specification Language, or more simply Frame Notation.
In contrast to the graphical emphasis of Statecharts, Frame is what I describe as a textual markdown language for system design that enables quickly specifying system behavior as automata. This approach has two major benefits:
- First, anyone with a text editor can create system specifications with Frame.
- Second, it eliminates the challenges of graphically modeling large systems, which at scale can be burdensome.
Frame is supported by a transpiler written in Rust and currently available online using WebAssembly at https://framepiler.frame-lang.org. In the near future, the project will be made open-source with the goal of building a community interested in advancing the technology further. As such, the Framepiler will also be available for use as part of development toolchains. Currently, the Framepiler can generate six object-oriented languages – C++, C#, JavaScript, Java, GDScript and Python as well as create limited UML diagrams. It is hoped that community support will rapidly expand and improve both aspects of the system.
Frame Philosophy
Frame notation is focused squarely on restructuring object-oriented classes to be state machines. By starting with what is arguably the atomic unit of system functionality in software today, Frame seeks to improve software development – one class at a time.
Frame syntax eschews unnecessary tokens as much as possible. The language does not use semicolons for statement termination or even commas for separators. How far this esthetic can be pushed remains to be seen, but early adopters seem to find the choices readable.
Frame syntax is also envisioned to try to evolve to be a symbolic language, sitting somewhere between the textual syntax of most programming languages and the visual approach of UML and other such modeling systems. Therefore the notation will favor incorporating syntax that is suggestive of the operations and functions that are highly information-dense. This is, to some extent, an ongoing exploration in ASCII art, but a rewarding one when a powerful syntax is identified that is compatible with prior notational selections.
It is hoped that better editor support for unicode symbols and other text entry approaches may unlock more opportunities to move towards a much more expressive and powerful programming symbology.
A Quick Primer on Frame Notation
Frame’s fundamental unit is a “system specification” document. Frame uses the # token to indicate an identifier is a Frame “system”:
#EmptySystem --- Three dashes indicate a comment line. --- This is an empty Frame specification --- The ## token indicates the end of a Frame system specification. ##
As previously mentioned, Frame notation is currently focused on restructuring object-oriented classes as state machines. To do so, Frame compartmentalizes system specs (and therefore the object-oriented classes they generate) into four structural blocks:
- Interface
- Machine
- Actions
- Domain
Here is a Frame spec (view on the Framepiler and select JavaScript) with those blocks declared and the empty (but well structured) system controller code it generates:
Frame Specification | Frame Controller |
---|---|
#BuildingBlocks -interface- -machine- -actions- -domain- ## |
let BuildingBlocks = function () { let that = {}; that.constructor = BuildingBlocks; //== Interface Block ==// //== Machine Block ==// //== Actions Block ==// //== Domain Block ==// return that; }; |
We will now discuss what Frame puts in these blocks to implement a state machine,
Frame Controller Structure
Frame system controllers are organized around the use of an internal message passing architecture. For an overview of this approach to state machine implementation, please check out my YouTube video on Getting Started With Frame.
We will now quickly examine each block and its role in a Frame controller.
The Interface Block
Frame does the following steps to process a call to an interface method:
- Create a “FrameEvent” object with the information from the interface call.
- Send event to the state machine.
- State machine processes event and attaches a response, if any.
- Interface returns response, if any, to the caller.
Frame events are very simple objects with just three necessary attributes:
- a message
- a parameters dictionary
- a return object
Here is a reference FrameEvent object implementation in JavaScript:
var FrameEvent = function(message, parameters) { var that = {}; that._message = message; that._parameters = parameters; that._return = null; return that; };
Here is a simple interface method that does not take any parameters showing the pattern:
//===================== Interface Block ===================// that.toggle = function () { let e = FrameEvent("toggle",null); _state_(e); }
As we can see, the FrameEvent is created with the message “toggle” and then sent into the state machine. Let’s examine what happens inside the state machine next.
The Machine Block
States are the skeletal system of a Frame spec and are indicated by a ‘$’ prefix:
Frame Specification | JavaScript Controller |
---|---|
$Off |
let _sOff_ = function (e) { } |
States contain zero or more event handlers. Events are selected to be handled by matching the message pattern inside pipes:
Frame Specification | JavaScript Controller |
---|---|
$Off |toggle| ^ |
let _sOff_ = function (e) { if (e._message == "toggle") { return; } } |
The ^ is the token for “return” and terminates the event handler. A more in depth description of Frame Event Handler Termination can be found on frame-lang.org.
Frame enter and exit event handlers are triggered by matching the special tokens |>| (enter) and |<| (exit). Here is a simple, but functional, Frame spec for a Lamp that shows this functionality:
Frame Specification | UML |
---|---|
#LampSpec -interface- toggle -machine- $Off |toggle| -> $On ^ $On |>| turnOn() ^ |<| turnOff() ^ |toggle| count = count + 1 log("Lamp used = " + count + "times.") -> $Off ^ -actions- turnOn turnOff log [msg] -domain- var count = 0 ## |
let LampSpec = function () { let that = {}; that.constructor = LampSpec; //== Interface Block ==// that.toggle = function () { let e = FrameEvent("toggle",null); _state_(e); } //== Machine Block ==// let _sOff_ = function (e) { if (e._message == "toggle") { _transition_(_sOn_); return; } } let _sOn_ = function (e) { if (e._message == ">") { that.turnOn_do(); return; } else if (e._message == "<") { that.turnOff_do(); return; } else if (e._message == "toggle") { that.count = that.count + 1; that.log_do("Lamp used = " + that.count + "times."); _transition_(_sOff_); return; } } //== Actions Block ==// that.turnOn_do = function () { throw new Error('Not implemented.'); } that.turnOff_do = function () { throw new Error('Not implemented.'); } that.log_do = function (msg) { throw new Error('Not implemented.'); } //== Domain Block ==// that.count = 0; //== Machinery and Mechanisms ==// let _state_ = _sOff_; let _transition_ = function(newState) { let exitEvent = FrameEvent("",null); _state_(enterEvent); } return that; }; |
You can see the Framepiler output of this spec as well as a working example on Codepen.
Hierarchical State Machines (HSMs)
Frame is inspired by the features available in UML Statecharts, though adapts them for ease of use and implementation. One of these key features is the Hierarchical State Machine, or HSM.
HSMs allow factoring common behavior between states into a “parent” state. Let us examine a simple state machine that demonstrates a typical problem with flat machines:
Frame Specification | UML |
---|---|
#UnfactoredMachine -machine- $A |b| -> "b" $B ^ |c| -> "c" $C ^ $B |a| -> "a" $A ^ |c| -> "c" $C ^ $C ## |
Above we see that the #UnfactoredMachine has two states ($A and $B) that duplicate an identical event handler that transitions to $C. To factor out this common behavior, Frame provides the “event dispatch” operator to send the current event to a parent class:
$ChildState => $ParentState
Here is the factored version of the previous machine:
Frame Specification | UML |
---|---|
#FactoredMachine -machine- $A => $AB |b| -> "b" $B ^ $B => $AB |a| -> "a" $A ^ $AB |c| -> "c" $C ^ $C ## |
Above we see that the #FactoredMachine now has a new state $AB that both $A and $B derive from. The way that this is implemented demonstrates one of the key reasons Frame utilizes the FrameEvent as part of its architecture:
Frame Specification | JavaScript Controller |
---|---|
... $A => $AB |b| -> "b" $B ^ ... $AB |c| -> "c" $C ^ ... |
... let _sA_ = function (e) { if (e._message == "b") { // b _transition_(_sB_); return; } _sAB_(e); } ... let _sAB_ = function (e) { if (e._message == "c") { // c _transition_(_sC_); return; } } ... |
As we can see, in the implementation the FrameEvent is easy to pass through a hierarchy of ancestor state functions, thus allowing the first state that is interested to respond. The #SelectiveResponseHSM spec below provides a simple example of child states selectively overriding the default behavior provided by a parent state:
You can see the generated code in the Framepiler and a working JavaScript demo is available on Codepen to see it in action.
The HSM implementation is easiest to understand starting with the default behavior in $AB and working up.
Frame Specification | JavaScript Controller |
---|---|
$AB |a| -> "a" $A ^ |b| -> "b" $B ^ |e1| stateAB_handledE1() ? log("$AB handled e1") ^ :: ^ |
let _sAB_ = function (e) { if (e._message == "a") { // a _transition_(_sA_); return; } else if (e._message == "b") { // b _transition_(_sB_); return; } else if (e._message == "e1") { if (that.stateAB_handledE1_do()) { that.log_do("$AB handled e1"); return; } return; } } |
The $AB state has event handlers for all of the interface events (execpt for the start message (|>>|). The. |a| and |b| messages simply transition the machine to their respective states, while |e1| tests if the AB state should handle the event and logs if it does.
Frame Specification | JavaScript Controller |
---|---|
$A => $AB |>| enterA() ^ |a| ^ |e1| stateA_handledE1() ? log("$A handled e1") ^ :: :>$B => $AB |>| enterB() ^ |b| ^ |e1| stateB_handledE1() ? log("$B handled e1") ^ :: :> |
let _sA_ = function (e) { if (e._message == ">") { that.enterA_do(); return; } else if (e._message == "a") { return; } else if (e._message == "e1") { if (that.stateA_handledE1_do()) { log("$A handled e1"); return; } }_sAB_(e); } let _sB_ = function (e) { if (e._message == ">") { that.enterB_do(); return; } else if (e._message == "b") { return; } else if (e._message == "e1") { if (that.stateB_handledE1_do()) { log("$B handled e1"); return; } }_sAB_(e); } |
The state hierarchy is shown above with states $A and $B looking very similar. Taking $A therefore as a proxy for the functionality in $B, $A demonstrates two key techniques for architecting HSM solutions in Frame:
- When in $A we don’t want to trigger another transition back into $A in response to an |a| message, which is what would happen if the event was passed onto $AB. Therefore |a| simply returns and short circuits the default behavior in $AB.
- The use of the continue operator :> as the terminator for event handler |e1| allows the handler to do some behavior – in this case test if the event was handled – but still pass on the event to the parent state for further processing if not. In this example, if stateA_handledE1() is true then the event is logged as processed and the event handler returns. Otherwise, the FrameEvent “falls out of” the event handler and is forwarded to the $AB state.
Conclusion and Next Steps
Hierarchical State Machines, and state machine implementation in general, is considered a very arcane art by most software developers. Frame makes it easy to specify sophisticated system design with a terse but highly intelligible notation that fuses the act of documenting a system with coding it.
You can find out more about Frame by visiting the website as well as watching an introductory video Getting Started With Frame that teaches the basics of Frame in about 10 minutes.
Please also consider joining the State Machines Reddit group and/or the Art of the StateDiscord group. I would enjoy talking Frame with you!
Head of solution architecture at Collinear Group a US based aerospace consultancy. My interests are software modeling with a focus on system behavior design, distributed systems and cloud architecture. I am the creator of Frame, a systems design markdown language and transpiler technology that will soon be an open source project.
from Hacker News https://ift.tt/3esRRjY
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.