Automatic machines against spaghetti code
- Transfer
“I love spaghetti westerns, I hate spaghetti code”
“Spaghetti code” is an ideal expression for describing software that is a smoking chaos from a cognitive and aesthetic point of view. In this article, I will talk about a three-point plan for destroying a spaghetti code:
- We discuss why the spaghetti code is not so tasty.
- Introducing a new look at what the code actually does.
- We are discussing Frame Machine Notation (FMN) , which helps developers unravel a ball of paste.
We all know how difficult it is to read someone else's code. This may be due to the fact that the task itself is difficult or because the structure of the code is too ... "creative". Often these two problems go hand in hand.
Challenges are difficult tasks, and usually nothing but a revolutionary discovery can simplify them. However, it happens that the software structure itself adds unnecessary complexity, and this problem is worth solving.
The ugliness of the spaghetti code lies in its complex conditional logic. And although life can be hard to imagine without the many tricky if-then-else constructs, this article will show you a better solution.
To illustrate the situation with spaghetti code, we need to first turn this:
Crispy Paste
In This:
Al dente!
Let's get started cooking.
Implicit state
To make pasta, we definitely need water for cooking. However, even such a seemingly simple element involving spaghetti code can be very confusing.
Here is a simple example:
(temp < 32)
What does this check really do? Obviously, it divides the number line into two parts, but what do these parts mean ? I think you can make a logical assumption, but the problem is that the code does not actually communicate this explicitly .
If I really confirm that she checks if the water is SOLID [approx. lane: according to the Fahrenheit scale, the water freezes at +32 degrees] , what will logically mean the return false?
if (temp < 32) {
// SOLID water
} else {
// not SOLID water. is (LIQUID | GAS)
}
Although the check divided the numbers into two groups, in fact there are three logical states - solid, liquid and gas (SOLID, LIQUID, GAS)!
That is, this number line:
split by condition check as follows:
if (temp < 32) {
} else {
}
Notice what happened because it is very important for understanding the nature of the spaghetti code. A Boolean check divided the number space into two parts, but did NOT categorize the system as a real logical structure from (SOLID, LIQUID, GAS). Instead, the check divided the space into (SOLID, everything else).
Here is a similar check:
if (temp > 212) {
// GAS water
} else {
// not GAS water. is (SOLID | LIQUID)
}
Visually, it will look like this:
if (temp > 212) {
} else {
}
Note that:
- the complete set of possible states is not announced anywhere
- nowhere in conditional constructs are verifiable logical states or groups of states declared
- some states are indirectly grouped by the structure of conditional logic and branching
Such code is fragile, but very common, and not so large as to cause problems with its support. So let's make the situation worse.
I still never liked your code.
The code shown above implies the existence of three states of matter - SOLID, LIQUID, GAS. However, according to scientific data, in fact, there are four observable states in which plasma (PLASMA) is included (in fact, there are many others, but this will be enough for us). Although no one is preparing a paste from plasma, if this code is published on Github, and then some post-graduate student studying high-energy physics will fork it, we will have to maintain this state too.
However, when plasma is added, the code shown above will naively do the following:
if (temp < 32) {
// SOLID water
} else {
// not SOLID water. is (LIQUID | GAS) + (PLASMA?)
// how did PLASMA get in here??
}
if (temp > 212) {
// GAS water + (PLASMA)
// again with the PLASMA!!
} else {
// not GAS water. is (SOLID | LIQUID)
}
It is likely that the old code, when added to many plasma states, will break in the else branches. Unfortunately, nothing in the code structure helps to report the existence of a new state or to influence changes. In addition, any bugs are likely to be inconspicuous, that is, finding them will be the most difficult. Just say no to the insects in the spaghetti.
In short, the problem is this: Boolean checks are used to determine states indirectly . Logical states are often not declared and not visible in code. As we saw above, when the system adds new logical states, existing code may break. To avoid this, developers should re-examine each individual conditional check and branch.to make sure the code paths are still valid for all their logical states! This is the main reason for the degradation of large code fragments as they become more complex.
Although there are no ways to completely get rid of conditional data checks, any technique that minimizes them will reduce code complexity.
Let's now take a look at a typical object-oriented implementation of a class that creates a very simple model of the volume of water. The class will manage changes in the state of the substance of water. Having studied the problems of the classical solution to this problem, we then discuss a new notation called Frame and show how it can cope with the difficulties we have discovered.
First bring the water to a boil ...
Science gave names to all the possible transitions that a substance can make when the temperature changes.
Our class is very simple (and not particularly useful). It answers the challenges of making transitions between states and changes the temperature until it becomes suitable for the desired target state:
(Note: I wrote this pseudo-code. Use it only at your own risk.)
class WaterSample {
temp:int
Water(temp:int) {
this.temp = temp
}
// gas -> solid
func depose() {
// If not in GAS state, throw an error
if (temp < WATER_GAS_TEMP)
throw new IllegalStateError()
// do depose
while (temp > WATER_SOLID_TEMP)
decreaseTemp(1)
}
// gas -> liquid
func condense() {
// If not in GAS state, throw an error
if (temp < WATER_GAS_TEMP)
throw new IllegalStateError()
// do condense
while (temp > WATER_GAS_TEMP)
decreaseTemp(1)
}
// liquid -> gas
func vaporize() {
// If not in LIQUID state, throw an error
if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP))
throw new IllegalStateError()
// do vaporize
while (temp < WATER_GAS_TEMP)
increaseTemp(1)
}
// liquid -> solid
func freeze() {
// If not in LIQUID state, throw an error
if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP))
throw new IllegalStateError()
// do freeze
while (temp > WATER_SOLID_TEMP)
decreaseTemp(1)
}
// solid -> liquid
func melt() {
// If not in SOLID state, throw an error
if (temp > WATER_SOLID_TEMP)
throw new IllegalStateError()
// do melt
while (temp < WATER_SOLID_TEMP)
increaseTemp(1)
}
// solid -> gas
func sublimate() {
// If not in SOLID state, throw an error
if (temp > WATER_SOLID_TEMP)
throw new IllegalStateError()
// do sublimate
while (temp < WATER_GAS_TEMP)
increaseTemp(1)
}
func getState():string {
if (temp < WATER_SOLID_TEMP) return "SOLID"
if (temp > WATER_GAS_TEMP) return "GAS"
return "LIQUID"
}
}
Compared to the first example, this code has certain improvements. First, the hard-coded “magic” numbers (32, 212) are replaced by the constants of state temperature boundaries (WATER_SOLID_TEMP, WATER_GAS_TEMP). This change begins to make states more explicit, albeit indirectly.
Checks for “defensive programming” also appear in this code, which restrict the method call if it is in an unsuitable state for the operation. For example, water cannot freeze if it is not a liquid - this violates the law (of nature). But the addition of watchdog conditions complicates the understanding of the purpose of the code. For instance:
// liquid -> solid
if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP))
throw new IllegalStateError()
This conditional check does the following:
- Checks if
temp
the GAS boundary temperature is less - Checks if the
temp
limit temperature is greater than SOLID - Returns an error if one of these checks is not true
This logic is confusing. Firstly, being in a liquid state is determined by what the substance is not - a solid or gas.
(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // is liquid?
Second, the code checks not whether liquid water to see if you need to return an error.
!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // Seriously?
The first time to understand this double negation of states is not easy. Here is a simplification that slightly reduces the complexity of the expression:
bool isLiquidWater = (temp < WATER_GAS_TEMP
&& temp > WATER_SOLID_TEMP)
if (!isLiquidWater) throw new IllegalStateError()
This code is easier to understand because the state isLiquidWater expressed clearly .
Now we are exploring techniques that fix an explicit state as the best way to solve problems. With this approach, the logical states of the system become the physical structure of the software, which improves the code and simplifies its understanding.
Frame machine notation
Frame Machine Notation (FMN) is a domain-specific language (Domain Specific Language, DSL) that defines a categorical, methodological and simple approach to defining and implementing various types of machines . For simplicity, I will call Frame automata simply “machines,” because this notation can define theoretical criteria for any different types (state machines, store automata, and the top evolution of automata — Turing machines). To know about the different types of machines and their application, I recommend to study the page on Wikipedia .
Although automata theory may be interesting (a VERY dubious statement), in this article we will focus on the practical application of these powerful concepts for building systems and writing code.
To solve this problem, Frame introduces a standardized notation that works at three integrated levels:
- Text DSL for defining Frame controllers with elegant and concise syntax
- A set of reference coding patterns for implementing object-oriented classes in the form of machines that Frame calls “controllers”
- Visual notation in which FMN is used to express complex operations that are difficult to represent graphically - Frame Visual Notation (FVN)
In this article, I will consider the first two points: FMN and reference patterns, and I will leave the discussion of FVN for future articles.
Frame is a notation that has several important aspects:
- FMN has first-level objects related to the concept of automata, which are not available in object-oriented languages.
- The FMN specification defines standard implementation patterns in pseudo code that demonstrate how FMN notation can be implemented.
- FMN will soon be able to compile (work in progress) in any object-oriented language
Note: the reference implementation is used to demonstrate the absolute equivalence of FMN notation and a simple way to implement it in any object-oriented language. You can choose any method.
Now I will introduce you to the two most important first-level objects in Frame - Frame Events and Frame Controllers .
Frame events
FrameEvents are an integral part of the simplicity of FMN notation. A FrameEvent is implemented as a structure or class that at least have the following member variables:
- message id
- dictionary or parameter list
- return object
Here is the pseudocode of the FrameEvent class:
class FrameEvent {
var _msg:String
var _params:Object
var _return:Object
FrameEvent(msg:String, params:Object = null) {
_msg = msg
_params = params
}
}
The frame notation uses the @ symbol , which identifies the FrameEvent object. Each of the required FrameEvent attributes has a special token to access it:
@|message| : вертикальные черты-скобки ссылаются на атрибут _msg
@[param1] : нотация [] позволяет разыменовывать список параметров переменных
@^ : знак возведения в степень используется здесь и в других контекстах для обозначения атрибута _return
Often we don’t have to specify what FrameEvent works with. Since most contexts work with only one FrameEvent at a time, the notation can definitely be simplified so that it uses only attribute selectors. Therefore, we can simplify access:
|buttonClick| // Select for a "buttonClick" event _msg
[firstName] = "Mark" // Set firstName _params property to "Mark"
^ = "YES" // Set the _return object to "YES"
Such a notation may seem strange at first, but soon we will see how such a simple syntax for events greatly simplifies the understanding of FMN code.
Frame controllers
A Frame Controller is an object-oriented class, ordered in a well-defined way to implement a Frame machine. Controller types are identified by the # prefix :
#MyController
this is equivalent to the following object oriented pseudocode:
class MyController {}
Obviously this class is not particularly useful. So that he can do something, the controller needs at least one state to respond to events.
The controllers are structured in such a way as to contain blocks of various types, which are identified by a dash surrounding the name of the block type:
#MyController
-block 1- -block 2- -block 3-
A controller can have no more than one instance of each block, and block types can contain only certain types of subcomponents. In this article, we examine only the -machine- block , which can only contain states. States are identified by the $ prefix token .
Here we see the FMN for a controller containing a machine with only one state:
#MyController // controller declaration
-machine- // machine block
$S1 // state declaration
Here is the implementation of the FMN code above:
class MyController {
// -machine-
var _state(e:FrameEvent) = S1 // initialize state variable
// to $S1
func S1(e:FrameEvent) { // state $S1 does nothing
}
}
The implementation of the machine block consists of the following elements:
- _state variable , which refers to a function of the current state. It is initialized with the first state function in the controller.
- one or more state methods
The Frame state method is defined as a function with the following signature:
func MyState(e:FrameEvent);
After defining these fundamentals of the implementation of the machine block, we can see how well the FrameEvent object interacts with the machine.
Interface unit
The interaction of FrameEvents that control the operation of the machine is the very essence of the simplicity and power of Frame notation. However, we have not yet answered the question, where do FrameEvents come from - how do they get into the controller to control it? One option: external clients themselves can create and initialize FrameEvents, and then directly call the method pointed to by the _state member variable:
myController._state(new FrameEvent("buttonClick"))
A much better alternative would be to create a common interface that wraps a direct call to the _state member variable:
myController.sendEvent(new FrameEvent("buttonClick"))
However, the most hassle-free way, corresponding to the usual way of creating object-oriented software, is to create common methods that send an event on behalf of the client to the internal machine:
class MyController {
func buttonClick() {
FrameEvent e = new FrameEvent("buttonClick")
_state(e)
return e._return
}
}
Frame defines the syntax for an interface block that contains methods that turn calls into a common interface for FrameEvents.
#MyController
-interface-
buttonClick
...
The block
interface
still has many other features, but this example gives us a general idea of how this works. I will give further explanation in the following articles of the series. Now let's continue studying the operation of the Frame automaton.
Event handlers
Although we have shown how to define a car, we do not yet have a notation with which to do anything . To process events, we need 1) to be able to select the event that needs to be processed and 2) to attach it to the behavior being performed.
Here is a simple Frame controller that provides the infrastructure for handling events:
#MyController // controller declaration
-machine- // machine block
$S1 // state declaration
|e1| ^ // e1 event handler and return
As said above, to access the attribute of the
_msg
FrameEvent event, FMN notation uses brackets from vertical lines:|messageName|
FMN also uses an exponent token representing the return statement. The controller shown above will be implemented as follows:
class MyController { // #MyController
// -machine-
var _state(e:FrameEvent) = S1
func S1(e:FrameEvent) { // $S1
if (e._msg == "e1") { // |e1|
return // ^
}
}
}
Here we see how clearly the FMN notation corresponds to an implementation pattern that is easy to understand and coding.
Having set these basic aspects of events, controllers, machines, states and event handlers, we can proceed to solve real problems with their help.
Single focus machines
Above we looked at a stateless controller that was pretty useless.
#MyController
One step higher in the food chain of utility is a class with a single state, which, although not useless, is simply boring. But at least he is at least doing something .
First, let's see how a class with only one (implied) state will be implemented:
class Mono {
String status() { return "OFF" }
}
No state is declared or even implied here, but let's assume that if the code does something, the system is in the “Working” state.
We will also introduce an important idea: interface calls will be considered similar to sending an event to an object. Therefore, the above code can be considered as a method of transmitting the | status | the Mono class, always in the $ Working state.
This situation can be visualized using the event binding table:
Now let's look at FMN, which demonstrates the same functionality and matches the same binding table:
#Mono
-machine-
$Working
|status|
^("OFF")
Here's what the implementation looks like:
class Mono { // #Mono
// -machine-
var _state(e:FrameEvent) = Working // initialize start state
func Working(e:FrameEvent) { // $Working
if (e._msg == "status") { // |status|
e._return = "OFF"
return // ^("OFF")
}
}
}
You can notice that we also introduced a new notation for the return statement , which means evaluating the expression and returning the result to the interface:
^(return_expr)
This operator is equivalent
@^ = return_expr
or simply
^ = return_expr
All these operators are functionally equivalent and you can use any of them, but it looks the most expressive
^(return_expr)
.Turn on the stove
So far we have seen a controller with 0 states and a controller with 1 state. They are not yet very useful, but we are already on the verge of something interesting.
To cook our pasta, you first need to turn on the stove. The following is a simple Switch class with a single boolean variable:
class Switch {
boolean _isOn;
func status() {
if (_isOn) {
return "ON";
} else {
return "OFF";
}
}
}
Although at first glance this is not obvious, the code shown above implements the following table of event bindings:
For comparison, here is an FMN for the same behavior:
#Switch1
-machine-
$Off
|status| ^("OFF")
$On
|status| ^("ON")
Now we see how exactly the Frame notation matches the purpose of our code - attaching an event (method call) to behavior based on the state in which the controller is located. In addition, the implementation structure also corresponds to the binding table:
class Switch1 { // #Switch1
// -machine-
var _state(e:FrameEvent) = Off
func Off(e:FrameEvent) { // $Off
if (e._msg == "status") { // |status|
e._return = "OFF"
return // ^("OFF")
}
}
func On(e:FrameEvent) { // $On
if (e._msg == "status") { // |status|
e._return = "ON"
return // ^("ON")
}
}
}
The table allows you to quickly understand the purpose of the controller in its various states. Both Frame notation structure and implementation pattern have similar advantages.
However, our switch has a noticeable functional problem. It is initialized in the state $ Off, but can not switch to the state $ On! To do this, we need to enter a state change operator .
Change state
The state change statement is as follows:
->> $NewState
Now we can use this operator to switch between $ Off and $ On:
#Switch2
-machine-
$Off
|toggle| ->> $On ^
|status| ^("OFF")
$On
|toggle| ->> $Off ^
|status| ^("ON")
And here is the corresponding event binding table:
New Event | toggle | now triggers a change that simply cycles through the two states. How can a state change operation be implemented?
Nowhere is easier. Here is the implementation of Switch2:
class Switch2 { // #Switch2
// -machine-
var _state(e:FrameEvent) = Off
func Off(e:FrameEvent) {
if (e._msg == "toggle") { // |toggle|
_state = On // ->> $On
return // ^
}
if (e._msg == "status") { // |status|
e._return = "OFF"
return // ^("OFF")
}
}
func On(e:FrameEvent) {
if (e._msg == "toggle") { // |toggle|
_state = Off // ->> $Off
return // ^("OFF")
}
if (e._msg == "status") { // |status|
e._return = "ON"
return // ^("ON")
}
}
}
You can also make the last improvement in Switch2 so that it not only allows you to switch between states, but also explicitly sets the state:
#Switch3
-machine-
$Off
|turnOn| ->> $On ^
|toggle| ->> $On ^
|status| ^("OFF")
$On
|turnOff| ->> $Off ^
|toggle| ->> $Off ^
|status| ^("ON")
Unlike the | toggle | event, if | turnOn | transmitted when Switch3 is already on or | turnOff | when it is already off, the message is ignored and nothing happens. This small improvement gives the client the ability to explicitly indicate the state in which the switch should be:
class Switch3 { // #Switch3
// -machine-
var _state(e:FrameEvent) = Off
/**********************************
$Off
|turnOn| ->> $On ^
|toggle| ->> $On ^
|status| ^("OFF")
***********************************/
func Off(e:FrameEvent) {
if (e._msg == "turnOn") { // |turnOn|
_state = On // ->> $On
return // ^
}
if (e._msg == "toggle") { // |toggle|
_state = On // ->> $On
return // ^
}
if (e._msg == "status") { // |status|
e._return = "OFF"
return // ^("OFF")
}
}
/**********************************
$On
|turnOff| ->> $Off ^
|toggle| ->> $Off ^
|status| ^("ON")
***********************************/
func On(e:FrameEvent) {
if (e._msg == "turnOff") { // |turnOff|
_state = Off // ->> $Off
return // ^
}
if (e._msg == "toggle") { // |toggle|
_state = Off // ->> $Off
return // ^
}
if (e._msg == "status") { // |status|
e._return = "ON"
return // ^("ON")
}
}
}
The final step in the evolution of our switch shows how easy it is to understand the purpose of the FMN controller. Relevant code demonstrates how easy it is to implement using Frame mechanisms.
Having created the Switch machine, we can turn on the fire and start cooking!
Sound state
A key, albeit subtle, aspect of automata is that the current state of the machine is the result of either a situation (for example, turning on) or some kind of analysis of data or the environment. When the machine has switched to the desired state, it is implied. that the situation will not change without the knowledge of the car.
However, this assumption is not always true. In some situations, verification (or “sensing”) of the data is required to determine the current logical state:
- initial restored state - when the machine is restored from a constant state
- external state - defines the “actual situation” that exists in the environment at the time of creation, restoration or operation of the machine
- volatile internal state - when part of the internal data managed by a running machine can change outside the control of the machine
In all these cases, data, environment, or both must be “probed” in order to determine the situation and set the state of the machine accordingly. Ideally, this Boolean logic can be implemented in a single function that defines the correct logical state. To support this pattern, Frame notation has a special type of function that probes the universe and determines the situation at the current time. Such functions are indicated by the $ prefix before the name of the method that returns a link to the state :
$probeForState()
In our situation, such a method can be implemented as follows:
func probeForState():FrameState {
if (temp < 32) return Solid
if (temp < 212) return Liquid
return Gas
}
As we can see, the method simply returns a reference to the state function corresponding to the correct logical state. This sensing function can then be used to enter the correct state:
->> $probeForState()
The implementation mechanism looks like this:
_state = probeForState()
The state sensing method is an example of Frame notation for managing state in a given way. Next, we will also learn the important notation for managing FrameEvents.
Behavioral inheritance and dispatcher
Behavioral inheritance and dispatcher are a powerful programming paradigm and the last topic about Frame notation in this article.
Frame uses uses inheritance of behavior , not inheritance of data or other attributes. For this state, FrameEvents are sent to other states if the initial state does not handle the event (or, as we will see in the next articles, just wants to pass it on). This chain of events can go to any desired depth.
For this, machines can be implemented using a technique called method chaining . FMN notation for sending events from one state to another is the dispatcher => :
$S1 => $S2
This FMN statement can be implemented as follows:
func S1(e:FrameEvent) {
S2(e) // $S1 => $S2
}
Now we see how easy it is to chain state methods. Let's apply this technique to a rather difficult situation:
#Movement
-machine-
$Walking => $Moving
|getSpeed| ^(3)
|isStanding| ^(true)
$Running => $Moving
|getSpeed| ^(6)
|isStanding| ^(true)
$Crawling => $Moving
|getSpeed| ^(.5)
|isStanding| ^(false)
$AtAttention => $Motionless
|isStanding| ^(true)
$LyingDown => $Motionless
|isStanding| ^(false)
$Moving
|isMoving| ^(true)
$Motionless
|getSpeed| ^(0)
|isMoving| ^(false)
In the above code, we see that there are two basic states - $ Moving and $ Motionless - and the other five states inherit important functionality from them. The event binding clearly shows us how the bindings in general will look:
Thanks to the techniques we have learned, the implementation will be very simple:
class Movement { // #Movement
// -machine-
/**********************************
$Walking => $Moving
|getSpeed| ^(3)
|isStanding| ^(true)
***********************************/
func Walking(e:FrameEvent) {
if (e._msg == "getSpeed") {
e._return = 3
return
}
if (e._msg == "isStanding") {
e._return = true
return
}
Moving(e) // $Walking => $Moving
}
/**********************************
$Running => $Moving
|getSpeed| ^(6)
|isStanding| ^(true)
***********************************/
func Running(e:FrameEvent) {
if (e._msg == "getSpeed") {
e._return = 6
return
}
if (e._msg == "isStanding") {
e._return = true
return
}
Moving(e) // $Running => $Moving
}
/**********************************
$Crawling => $Moving
|getSpeed| ^(.5)
|isStanding| ^(false)
***********************************/
func Crawling(e:FrameEvent) {
if (e._msg == "getSpeed") {
e._return = .5
return
}
if (e._msg == "isStanding") {
e._return = false
return
}
Moving(e) // $Crawling => $Moving
}
/**********************************
$AtAttention => $Motionless
|isStanding| ^(true)
***********************************/
func AtAttention(e:FrameEvent) {
if (e._msg == "isStanding") {
e._return = true
return
}
Motionless(e) // $AtAttention => $Motionless
}
/**********************************
$LyingDown => $Motionless
|isStanding| ^(false)
***********************************/
func LyingDown(e:FrameEvent) {
if (e._msg == "isStanding") {
e._return = false
return
}
Motionless(e) // $AtAttention => $Motionless
}
/**********************************
$Moving
|isMoving| ^(true)
***********************************/
func Moving(e:FrameEvent) {
if (e._msg == "isMoving") {
e._return = true
return
}
}
/**********************************
$Motionless
|getSpeed| ^(0)
|isMoving| ^(false)
***********************************/
func Motionless(e:FrameEvent) {
if (e._msg == "getSpeed") {
e._return = 0
return
}
if (e._msg == "isMoving") {
e._return = false
return
}
}
}
Water machine
Now we have the basics of knowledge about FMN, allowing us to understand how to re-implement the WaterSample class with states and in a much more intelligent way. We will also make it useful for our graduate student physicist and add a new $ Plasma state to it:
Here's what the full FMN implementation looks like:
#WaterSample
-machine-
$Begin
|create|
// set temp to the event param value
setTemp(@[temp])
// probe for temp state and change to it
->> $probeForState() ^
$Solid => $Default
|melt|
doMelt() ->> $Liquid ^
|sublimate|
doSublimate() ->> $Gas ^
|getState|
^("SOLID")
$Liquid => $Default
|freeze|
doFreeze() ->> $Solid ^
|vaporize|
doVaporize() ->> $Gas ^
|getState|
^("LIQUID")
$Gas => $Default
|condense|
doCondense() ->> $Liquid ^
|depose|
doDepose() ->> $Solid ^
|ionize|
doIonize() ->> $Plasma ^
|getState|
^("GAS")
$Plasma => $Default
|recombine|
doRecombine() ->> $Gas ^
|getState|
^("PLASMA")
$Default
|melt| throw new InvalidStateError()
|sublimate| throw new InvalidStateError()
|freeze| throw new InvalidStateError()
|vaporize| throw new InvalidStateError()
|condense| throw InvalidStateError()
|depose| throw InvalidStateError()
|ionize| throw InvalidStateError()
|recombine| throw InvalidStateError()
|getState| throw InvalidStateError()
As you can see, we have the initial state of $ Begin, which responds to the message | create | and retains value
temp
. The sensing function first checks the initial value temp
to determine the logical state, and then performs the transition of the machine to this state. All physical states ($ Solid, $ Liquid, $ Gas, $ Plasma) inherit protective behavior from the $ Default state. All events that are not valid for the current state are passed to the $ Default state, which throws an InvalidStateError error. This shows how simple defensive programming can be implemented using behavior inheritance.
And now the implementation:
class WaterSample {
// -machine-
var _state(e:FrameEvent) = Begin
/**********************************
$Begin
|create|
// set temp to the event param value
setTemp(@[temp])
// probe for temp state and change to it
->> $probeForState() ^
***********************************/
func Begin(e:FrameEvent) {
if (e._msg == "create") {
setTemp(e["temp"])
_state = probeForState()
return
}
}
/**********************************
$Solid => $Default
|melt|
doMelt() ->> $Liquid ^
|sublimate|
doSublimate() ->> $Gas ^
|sublimate|
^("SOLID")
***********************************/
func Solid(e:FrameEvent) {
if (e._msg == "melt") {
doMelt()
_state = Liquid
return
}
if (e._msg == "sublimate") {
doSublimate()
_state = Gas
return
}
if (e._msg == "getState") {
e._return = "SOLID"
return
}
Default(e)
}
/**********************************
$Liquid => $Default
|freeze|
doFreeze() ->> $Solid ^
|vaporize|
doVaporize() ->> $Gas ^
|getState|
^("LIQUID")
***********************************/
func Liquid(e:FrameEvent) {
if (e._msg == "freeze") {
doFreeze()
_state = Solid
return
}
if (e._msg == "vaporize") {
doVaporize()
_state = Gas
return
}
if (e._msg == "getState") {
e._return = "LIQUID"
return
}
Default(e)
}
/**********************************
$Gas => $Default
|condense|
doCondense() ->> $Liquid ^
|depose|
doDepose() ->> $Solid ^
|ionize|
doIonize() ->> $Plasma ^
|getState|
^("GAS")
***********************************/
func Gas(e:FrameEvent) {
if (e._msg == "condense") {
doCondense()
_state = Liquid
return
}
if (e._msg == "depose") {
doDepose()
_state = Solid
return
}
if (e._msg == "ionize") {
doIonize()
_state = Plasma
return
}
if (e._msg == "getState") {
e._return = "GAS"
return
}
Default(e)
}
/**********************************
$Plasma => $Default
|recombine|
doRecombine() ->> $Gas ^
|getState|
^("PLASMA")
***********************************/
func Plasma(e:FrameEvent) {
if (e._msg == "recombine") {
doRecombine()
_state = Gas
return
}
if (e._msg == "getState") {
e._return = "PLASMA"
return
}
Default(e)
}
/**********************************
$Default
|melt| throw new InvalidStateError()
|sublimate| throw new InvalidStateError()
|freeze| throw new InvalidStateError()
|vaporize| throw new InvalidStateError()
|condense| throw InvalidStateError()
|depose| throw InvalidStateError()
|ionize| throw InvalidStateError()
|recombine| throw InvalidStateError()
|getState| throw InvalidStateError()
***********************************/
func Default(e:FrameEvent) {
if (e._msg == "melt") {
throw new InvalidStateError()
}
if (e._msg == "sublimate") {
throw new InvalidStateError()
}
if (e._msg == "freeze") {
throw new InvalidStateError()
}
if (e._msg == "vaporize") {
throw new InvalidStateError()
}
if (e._msg == "condense") {
throw new InvalidStateError()
}
if (e._msg == "depose") {
throw new InvalidStateError()
}
if (e._msg == "ionize") {
throw new InvalidStateError()
}
if (e._msg == "recombine") {
throw new InvalidStateError()
}
if (e._msg == "getState") {
throw new InvalidStateError()
}
}
}
Conclusion
Automata is a basic concept of computer science that has been used for too long only in specialized areas of software and hardware development. The main task of Frame is to create a notation for describing automata and setting simple patterns for writing code or “mechanisms” for their implementation. I hope that the Frame notation will change the way programmers look at machines, providing an easy way to put them into practice in everyday programming tasks and, of course, save them from spaghetti in the code.
Terminator eats pasta (photo by Suzuki san)
In future articles, based on the concepts we have learned, we will create even greater power and expressiveness of FMN notation. Over time, I will expand the discussion to the study of visual modeling, which includes FMN and solves the problems of uncertain behavior in modern approaches to software modeling.