📜 ⬆️ ⬇️

Spaghetti Automatic Machines


“I love spaghetti westerns, I hate spaghetti code”

“Spaghetti code” is an ideal expression for describing software that represents smoking chaos from both a cognitive and aesthetic point of view. In this article I will talk about a three-point plan for the destruction of spaghetti code:


We all know how hard it is to read someone else's code. This may be due to the fact that the task itself is complex or because the structure of the code is too ... "creative." Often these two problems go hand in hand.
')
Difficult tasks are complex 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 spaghetti code lies in its complex conditional logic. And although life can be difficult to imagine without a lot of clever if-then-else constructions, this article will show you a better solution.


To illustrate the situation with spaghetti code, we need to first turn it:


Crispy Pasta

In it:


Al Dente!

Let's get down to cooking.

Implicit state


To cook pasta, we definitely need water for cooking. However, even such a seemingly simple element with the participation of spaghetti code can be very confusing.

Here is a simple example:

(temp < 32) 

What does this check actually 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 it checks whether water is FIRM [approx. lane: Fahrenheit water freezes at +32 degrees] , what would logically mean returning 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:


divided by the condition check as follows:

 if (temp < 32) { 


 } else { 


 } 

Notice what happened, because it is very important to understand the nature of spaghetti code. The Boolean test divided the numerical space into two parts, but did NOT categorize the system as a real logical structure from (SOLID, LIQUID, GAS). Instead, the test 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 { 


 } 

Notice that:

  1. the full set of possible states is not declared anywhere
  2. nowhere in conditional constructions verifiable logical states or groups of states are declared
  3. some states are indirectly grouped by the structure of conditional logic and branching

Such code is fragile, but very common, and not large enough to cause problems with its support. So let's make the situation worse.


I never liked your code anyway

The code shown above implies the existence of three states of matter - SOLID, LIQUID, GAS. However, according to scientific data, there are actually four observable states in which plasma is switched on (PLASMA) (in fact there are many others, but this will be enough for us). Although no one is preparing a plasma paste, if this code is published on Github, and then some graduate student who studies high-energy physics will fork it, we will have to maintain this state too.

However, when adding plasma, the above code 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 the set of plasma states, will break in the branching else. Unfortunately, nothing in the code structure helps to inform about the existence of a new state or to influence changes. In addition, any bugs are likely to be barely noticeable, that is, they will be the most difficult to find. Just say no to insects in spaghetti.

In short, the problem is the following - Boolean checks are used to determine the state indirectly . Logical states are often not declared and not visible in the code. As we saw above, when the system adds new logical states, the existing code may break. To avoid this, developers must re-examine every single conditional check and branch to make sure that the code paths are still valid for all the corresponding logical states! This is the main reason for the degradation of large code fragments with their complexity.

Although there are no ways to completely get rid of conditional data checks, any technique that minimizes them will reduce the complexity of the code.

Let's now look at a typical object-oriented implementation of a class that creates a very simple model for the volume of water. The class will manage changes in the state of the water substance. Having studied the problems of the classical solution of this problem, we will then discuss the new notation called Frame and show how it can cope with the difficulties we found.

First bring the water to a boil ...


Science has given names to all possible transitions that a substance can make when temperature changes.


Our class is very simple (and not very useful). It responds to calls to perform state transitions and changes the temperature until it becomes suitable for the desired target state:

(Note: I wrote this pseudocode. Use it at work only at your own peril and 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 constants of the boundaries of the temperature of the states (WATER_SOLID_TEMP, WATER_GAS_TEMP). This change begins to make states more pronounced, albeit indirectly.

This code also contains “defensive programming” checks that limit the method call if it is in the wrong state for an operation. For example, water cannot freeze if it is not a liquid - it violates the law (of nature). But adding watchdog conditions complicates understanding the purpose of the code. For example:

 // liquid -> solid if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() 

This conditional check does the following:

  1. Checks if temp lower than GAS boundary temperature
  2. Checks if temp greater than SOLID limit temperature
  3. Returns an error if any of these checks were not true.

This logic is confusing. First, being in a liquid state is determined by what a substance is not - a solid or gas.

 (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // is liquid? 

Secondly, the code checks whether the water is liquid, to see if it is necessary to return an error.

 !(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // Seriously? 

From the first time to understand this double negation of states is not at all 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 isLiquidWater state is explicitly expressed.

Now we investigate 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), defining a categorical, methodological and simple approach to defining and implementing various types of automata . For simplicity, I will call the Frame automata simply “machines”, because this notation can define theoretical criteria for any different types (state machines, store automats, and the top of the evolution of automata — Turing machines). To know about the various types of machines and their application, I recommend to explore the page in Wikipedia .

Although automaton theory may be interesting (a VERY dubious statement), in this article we will focus on the practical application of these powerful concepts for creating systems and writing code.

To solve this problem, Frame introduces a standardized notation that works on three integrated levels:

  1. Text DSL for specifying Frame controllers using elegant and concise syntax
  2. A set of reference coding patterns for implementing object-oriented classes in the form of machines, which Frame calls "controllers"
  3. Visual notation in which FMN is used to express complex operations that are difficult to visualize graphically - Frame Visual Notation (FVN)

In this article I will look at the first two points: FMN and reference patterns, and leave the discussion for FVN for future articles.

Frame is a notation, distinguished by some important aspects:

  1. FMN has first-level objects related to the concept of automata, which are absent from object-oriented languages.
  2. The FMN specification defines standard implementation patterns in pseudocode that demonstrate how FMN notation can be implemented.
  3. 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 objects of the first level in Frame - Frame Events and Frame Controllers .

Frame events


FrameEvents are an integral part of the simplicity of FMN notation. FrameEvent is implemented as a structure or class that at least has the following member variables:


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 to identify the FrameEvent object. Each of the required attributes of FrameEvent has a special token to access it:

 @|message| :  -    _msg @[param1] :  []      @^ :              _return 

Often we don’t have to specify what the FrameEvent works with. Since most contexts work with only one FrameEvent at a time, the notation can be unambiguously 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 we will soon see how such a simple syntax for events greatly simplifies the understanding of the FMN code.

Frame controllers


Frame Controller is an object-oriented class, ordered in a well-defined way for the implementation of the 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 type name of the block:

 #MyController<br> -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 block- machine- , which can contain only states. States are identified by the $ prefix token.

Here we see 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 above FMN code:

 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:

  1. _state variable that refers to the current state function. It is initialized with the first state function in the controller.
  2. one or more state methods

The frame state method is defined as a function with the following signature:

 func MyState(e:FrameEvent); 

After setting these fundamentals to the implementation of the machine block, we can see how well the FrameEvent object interacts with the machine.

Interface block


The interaction of FrameEvents controlling the operation of a 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 creating general methods that send an event on behalf of a client to an 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 interface block has many other features, but this example gives us a general idea of ​​how this works. I will give a further explanation in the following articles of the series.

Now let's continue studying the work of the Frame machine.

Event handlers


Although we have shown how to define a car, we do not yet have a notation with which to do anything. To handle events, we need to 1) be able to select the event that needs to be processed and 2) bind 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 mentioned above, to access the _msg attribute of a _msg event, the FMN notation uses brackets of vertical lines:

 |messageName| 

FMN also uses an exponent mark token that denotes a 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 the implementation pattern, which is easy to understand and coding.

Having set these basic aspects of events, controllers, machines, states, and event handlers, we can proceed to solving real problems with their help.

Single-focus machines


Above, we looked at a stateless controller, which was rather 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 does at least 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: the interface calls will be considered similar to sending an event to an object. Therefore, the above code can be viewed as a way to send the | status | the Mono class, which is always in the $ Working state.

You can visualize this situation using the event bindings table:


Now let's look at FMN, which demonstrates the same functionality and corresponds to the same table of bindings:

 #Mono -machine- $Working |status| ^("OFF") 

Here is the implementation:

 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 see that we also introduced a new notation for the operator return , which means the calculation of the expression and the return of the result to the interface:

 ^(return_expr) 

This statement is equivalent to

 @^ = return_expr 

or simply

 ^ = return_expr 

All these operators are functionally equivalent and you can use any of them, but ^(return_expr) looks the most expressive.

Turn on the stove


So far we have seen a controller with 0 states and a controller with 1 state. They are not very useful yet, but we are already on the verge of something interesting.

To cook our pasta, you need to first 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 not obvious at first glance, the code shown above implements the following event binding table:


For comparison, here is the FMN for the same behavior:

 #Switch1 -machine- $Off |status| ^("OFF") $On |status| ^("ON") 

Now we see how exactly the Frame notation corresponds to the purpose of our code — to bind 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 the Frame notation structure and the implementation pattern have similar advantages.

However, our switch has a noticeable functional issue. It is initialized in the $ Off state, but cannot switch to the $ On state! To do this, we need to enter a state change operator.

We change a 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 between two states. How can you implement a state change operation?

There is simply no place. 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 latest enhancement 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 allows the client 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 last stage in the evolution of our switch shows how easy it will be to understand the purpose of the FMN controller. The 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!

We probe the state


A key, albeit subtle, aspect of automata is that the current state of the machine is the result of either the situation (for example, switching on) or some kind of data analysis or environment. When the machine has switched to the desired state, it is implied. that the situation will not change without the knowledge of the machine.

However, this assumption is not always true. In some situations, validation (or “probing”) of data is required to determine the current logical state:

  1. original restored state - when the machine is restored from a permanent state
  2. external state - determines the "actual situation" that exists in the environment at the time of creation, restoration or operation of the machine
  3. changeable internal state - when part of the internal data managed by a running machine may change outside the control of the machine

In all these cases, the data, the 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 determines the correct logical state. To support this pattern, the 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 reference to the state :

 $probeForState() 

In our situation, this method can be implemented as follows:

 func probeForState():FrameState { if (temp < 32) return Solid if (temp < 212) return Liquid return Gas } 

As we see, the method simply returns a reference to the state function corresponding to the correct logical state. This probe function can then be used to go to the right state:

 ->> $probeForState() 

The implementation mechanism looks like this:

 _state = probeForState() 

The state sensing method is an example of Frame notation for managing a state in a specified way. Further we will also study the important notation for managing FrameEvents.

Behavior Inheritance and Dispatcher


Behavior inheritance and dispatcher is a powerful programming paradigm and the last topic about Frame notation in this article.

Frame uses inheritance behaviors , not inheritance of data or other attributes. For this state, FrameEvents is sent to other states if the initial state does not handle the event (or, as we will see in the following articles, just wants to pass it on). This chain of event transmission can go to any desired depth.

For this machine can be implemented using a technique called method chaining . FMN notation for sending an event from one state to another is dispatcher => :

 $S1 => $S2 

This FMN operator can be implemented as follows:

 func S1(e:FrameEvent) { S2(e) // $S1 => $S2 } 

Now we see how easy it is to chain the 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 code shown above, 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 like:


Thanks to the techniques we studied, 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 FMN knowledge, allowing us to understand how to reimplement 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 the new $ Plasma state to it:


Here is the full implementation of FMN:

 #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 stores the value temp. The probe function first checks the initial value tempto 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 gives 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, which 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 programmers' view of automata by providing an easy way to practice them in everyday programming tasks, and, of course, it will save them from spaghetti in the code.


Terminator eats pasta (photo by Suzuki san)
In future articles, based on the concepts studied, we will create even more 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 problem of uncertain behavior in modern approaches to software modeling.

Source: https://habr.com/ru/post/446330/


All Articles