Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Configuring Simple State Machines

0.00/5 (No votes)
6 May 2020 1  
An attempt to throw some light on how state machines work and what they can be used for
An illustration of how to employ State Machines for basic pattern recognition followed by a look at more advanced implementations such as data validation, the use of sub-states and the implementation of asynchronous action methods when transitioning into and out of a state.

Introduction

Complex systems can often be simplified by breaking them down into a series of discrete stages or states with transitions from state to state occurring as the system progresses. A state machine's function is to manage these transitions in response to some sort of input trigger. Ideally, the state machine should know very little about the states and their functionality, it just keeps a reference to the current state and directs all input towards that state. When a transition is made to another state, that state becomes the current state and inputs are directed towards it. The input triggers determine the next selected state, so, if modelling the production of a widget, the quality-control state could transition to the dispatch state or recycling state depending on the trigger received. To illustrate this, have a look at the UML State Machine Diagram of one of the purest forms of the state machine.

Image 1

The Ancestral State Machine

This is the basic form of the state machine from which modern versions have evolved. There are only two triggers, a binary 1 or 0. The way to read the diagram is to start at the starting state and follow the transitions from state to state. State A is the starting state, the initial current state. It's marked by having an arrow pointing to it that doesn't come from another state. If this state receives a 0, there is no transition as indicated by the arrow pointing back towards its origin. A 1 causes the machine to make state B the current state. The inputs are now applied to state B where the response is different. A 0 trigger here causes a transition back to state A and a 1 causes a transition to state C. From state C, a 1 goes to state D and a 0 to state A. State D is the end state, it has no transitions to another state. End states are marked by two concentric circles, there can be more than one end state but there is only one start state. So what's the machine doing? It's detecting the pattern 111 within a string of binary inputs. If, at the end of the data stream, the current state is D the input is accepted, if not it's rejected. Notice that there's no memory in the system, the states are unaware of each other and all transitions are triggered by external inputs. The states have no intrinsic functionality; they only act as a target identifier for the triggers.

A Brief Introduction to Stateless

The following examples use the popular state machine Stateless that's available for download as a NuGet package. The machine itself is generic, you need to provide the type of the States and the type of the Triggers as well as the starting State when it is initialised. Simple applications usually define an enum to represent the States and another enum to represent the Triggers. The enums are then used as labels to relate to internally managed pseudo-states and triggers. This arrangement removes the need to define a common interface for all states - a difficult task at the best of times as the whole point of transitioning to a new state is to implement a different functionality.

Verifying an Email Address Using a Simple State Machine

This example shows how the basic state machine can be expanded to enable it to verify an email address. The context class is EmailValidator, it encapsulates the state machine and takes a string inputted from an external source. The string is iterated over and its characters are used as triggers, the string is verified if the machine ends up in an acceptable state when the iteration has finished. The triggers are chars and the States are an enum.

public enum EmailState
{
    Start,
    Local,
    Domain,
    Accepted,
    Rejected
}

public class EmailValidator : IValidator
{
 private readonly StateMachine<EmailState, char> machine;
 public EmailValidator()
 {
   machine = new StateMachine<EmailState, char>(EmailState.Start);
   // ignore unconfigured Trigger exception
   machine.OnUnhandledTrigger((state, trigger) => { });
   ConfigureMachine();
}

To simplify matters, all illegal characters are filtered out before triggering the machine so the machine can concentrate on determining if an email address is correctly formatted.

 public bool Validate(string dataString)
 {
  char[] acceptable = new char[] { '@', '.', '-' };
  //rinse out all illegal chars
  if (dataString.Any(c => !char.IsLetterOrDigit(c) && !acceptable.Contains(c))
   {
    return false;
   }

  foreach (var c in dataString)
  {
   //use the trigger 'x' for all alphanumeric chars
   char  trigger = char.IsLetterOrDigit(c) ? 'x' : c;
   //The Fire method initiates the state transition.
   machine.Fire(trigger);
  }
   var isValid = machine.IsInState(EmailState.Accepted);
   //reset to Start
   currentState = EmailState.Start;
   return isValid;
}

Image 2

The state machine diagram above shows how the machine is 'wired up'. The first character is inputted into the Start state. The characters @, dot and hyphen are rejected; all others are accepted and result in a transition to the Local state that deals with the local part of the email address. The Local state accepts all chars apart from @, @ causes a transition to the Domain state. The first character in the Domain state has to be alphanumeric. The Accepted state accepts everything apart from a hyphen and @.

Configuring the State Machine

States are configured by simply permitting the transitions that are allowed from each state using the Permit method that takes a trigger and a state as parameters.

private void ConfigureMachine()
       {
           machine.Configure(EmailState.Start)
           .Permit('@', EmailState.Rejected)
           .Permit('.', EmailState.Rejected)
           .Permit('x', EmailState.Local);

           machine.Configure(EmailState.Local)
           .Permit('@', EmailState.Domain);
           .......
        }

State Machine Diagrams

State Machine diagrams are a great debugging aid as they enable you to visualise the machine's configuration to the extent that someone with no prior knowledge of the configuration can see exactly how the machine is set up. Stateless has a method, UmlDotGraph.Format(machine.GetInfo()), that outputs a Dot formatted string which, when pasted into the text box at Webgraphviz, will generate a graph. There is a free app available from the site that you can download to do the same thing.

Using States to Process Data

In the previous examples, the states are silent, they don't actually do any work. But, for states to be used in some form of production line, each state needs to be able to carry out work under the direction of the Context class. The way this is achieved in Stateless is by the use of two Action delegates, the OnEntry and OnExit Action delegates. OnEntry is called when the state is transitioned into and the OnExit method is called when the state is transitioned out of. It may be thought that the Action delegates would not be very useful as they have no parameters and return void. But that's not the case as the delegates are instantiated in the Context and so they are able to capture all of the Context's public and private variables.

private readonly IValidator validator = new EmailValidator();
 .....
 machine.Configure(State.Validating)
 .OnEntry(() =>
   {
     //prompt for email address
     var address= Console.ReadLine();
     Trigger trigger = validator.Validate(address) ? Trigger.Accept : Trigger.Fail;
     machine.Fire(trigger);
   })
 ....

An important point is that the Validator knows nothing about the State or the machine's OnEntry method that's associated with the State. In order to maintain a good separation of concerns, all transitions should be handled by the Context. It's not a good idea to allow any helper class such as the Validator to become trigger happy and start firing on its own volition.

A Validation Example

Image 3

This example mimics the some sort of validation process where the applicant has three chances to input a valid email address. After every unsuccessful attempt, the user has the option to cancel or retry. Three failed attempts result in the application being rejected. The Validating state is configured to use the trigger Fail as a guard trigger. Its transition depends upon the bool returned from the IsRejected method. If IsRejected returns true, the trigger Fail causes a transition to the Rejected end state. A false value causes a transition to the Failed state where there is an option to cancel or to try again.

machine.Configure(State.Validating)
.OnEntry(()
{
    //prompt for an input
    Tweet(Constants.StartValidating);
    var address= Console.ReadLine();
    Trigger trigger = validator.Validate(address) ? Trigger.Accept : Trigger.Fail;
    machine.Fire(trigger);
})
.Permit(Trigger.Accept, State.Accepted)
.PermitIf(Trigger.Fail, State.Failed, () => !IsRejected)
.PermitIf(Trigger.Fail, State.Rejected, () => IsRejected);

Guard triggers are configured using the PermitIf method. My preference is not to use them and to keep all the logic within the OnEntry Action rather than let it escape into some sort of guided missile type trigger that hasn't got a predefined target.

Managing the Current State

In the validation example, the current state needs to be managed externally to the StateMachine class so that it can be reset to the Start state before each attempt at validation. Here's how to set it up, it's just a matter of providing a getter and setter as parameters to the constructor.

private EmailState currentState= EmailState.Start;
private readonly StateMachine<EmailState, char> machine;
public EmailValidator()
 {
  //provide a getter and setter so the currentState can be reset to the Start State
  //after each attempt at validation
  machine = new StateMachine<EmailState, char>(() => currentState, s => currentState = s);
  // ignore unconfigured Trigger exception
  machine.OnUnhandledTrigger((state, trigger) => { });
  ConfigureMachine();
 }

Substates

Image 4

It's possible to designate a state as being a substate of some other state. The effect of this is that the superstate's OnExit method is not called when the current state transitions from the superstate to the substate. In this example, SeatBelt is a substate of Motoring, Engine is a substate of SeatBelt and Brake is a substate of Engine. The substate status is inherited so SeatBelt, Engine and Brake are all substates of Motoring. When the Park trigger is fired, OnExit methods will be called consecutively, bubbling up from the Brake state to the Motoring state. Typically, there would just be the one external transition permitted into the Motoring state and its substates but there can be multiple exit triggers.

  private void ConfigureMachine()
        {
            machine.Configure(State.Start)
            .Permit(Trigger.Motor, State.Motoring)
            .OnEntry(() => Console.WriteLine("In State Start"))
            .OnExit(() => Console.WriteLine("Leaving Start"));

            machine.Configure(State.Motoring)
            .Permit(Trigger.Fasten, State.Seatbelt)
            .OnEntry(() => Console.WriteLine("Started Motoring"))
            .OnExit(() => Console.WriteLine("Finished Motoring"));

            machine.Configure(State.Seatbelt)
            .SubstateOf(State.Motoring)
            .Permit(Trigger.Engage, State.Engine)
            .OnEntry(() => Console.WriteLine("Seatbelt Fastened"))
            .OnExit(() => Console.WriteLine("Seatbelt Unfastened"));


            machine.Configure(State.Engine)
            .SubstateOf(State.Seatbelt)
            .Permit(Trigger.Release, State.Brake)
            .OnEntry(() => Console.WriteLine("Engine Started"))
            .OnExit(() => Console.WriteLine("Engine Off"));

            machine.Configure(State.Brake)
            .SubstateOf(State.Engine)
            .Permit(Trigger.Park, State.Parked)
            .OnEntry(() => Console.WriteLine("Brake Released"))
            .OnExit(() => Console.WriteLine("Brake Applied"));

            machine.Configure(State.Parked)
           .OnEntry(() => Console.WriteLine("Parked"));
}

Asynchronous OnExitAsync and OnEntryAsync Actions

The previous example could be refined by keeping the engine running and only stop it after the brakes have been applied. To do this, the Engine state needs to remain active when it's no longer the current state. So there's a need to run the Engine's OnEntry Action asynchronously and to end it when the state's OnExit method is called. The way to achieve this is to use the OnExitAsync Func and the FireAsync method.

 CancellationTokenSource cts = new CancellationTokenSource();
.....
          machine.Configure(State.Engine)
          .SubstateOf(State.Seatbelt)
          .Permit(Trigger.Release, State.Brake)
          .OnEntry( () =>
          {
              //start the task but don't await it here
              engineTask = Task.Run(()=>ChugChug(cts.Token));
              Log($"Engine Started {engineNoise}");
          })
          .OnExitAsync(async() =>
          {
            cts.Cancel();
            await engineTask;
            Log("Engine Stopped");
          });

The ChugChug method is just a bit of nonsense that simulates the engine running.

private void ChugChug(CancellationToken token)
{
    while (true)
    {
        //simulate long-running method
       Thread.Sleep(5);
        //check for cancellation
        if (token.IsCancellationRequested) break;
        Console.ForegroundColor = ConsoleColor.White;
        Console.Write(engineNoise);
    }
}

When there is a transition from the Brake state to the Parked state, the OnExitAsync Func of the current state is called first, then the call bubbles up through the substates to the Motoring state. So the OnExitAsync Func of all the Motoring states except Engine need to be configured in a similar way to this.

machine.Configure(State.Brake)
.SubstateOf(State.Engine)
.Permit(Trigger.Park, State.Parked)
.OnEntry(() => Log("Brake Released"))
.OnExitAsync(() =>
 {
     Log("BreakApplied");
     //the method expects a Task to be returned
     return Task.CompletedTask;
 });

The StartupAsync method transitions the current state from the Start state to the Parked state.

public async Task StartupAsync()
  {
     machine.Fire(Trigger.Motor);
     machine.Fire(Trigger.Fasten);
     machine.Fire(Trigger.Engage);
     machine.Fire(Trigger.Release);
     string msg = machine.IsInState(State.Motoring) ? "is in " : "is not in ";
     Log($"The current state is {machine.State}
         it {msg}state Motoring",ConsoleColor.Yellow);
     await Task.Delay(50);
     Log("\r\nFiring Trigger Park",ConsoleColor.Yellow);
      //FireAsync calls the OnExitAsync action of the current state
      //The call bubbles up through the substates to State.Motoring
      await machine.FireAsync(Trigger.Park);
  }

The output from StartupAsync is mainly frivolous but it gives an idea of how much functionality can be unleashed by simply firing a trigger.

Image 5

Conclusion

State machines are useful for breaking the code up into a series of discrete sections and progressing the code from section to section. It’s true that configuring the machine needs some care and thought but, once set, it is unlikely to be corrupted by any other user-written code. The ability of state machines to produce a visual representation of the states and their transition triggers is a valuable asset as it provides a ‘wiring diagram’ of how the system is set up and greatly simplifies its maintenance and expansion. A state machine is not a panacea for all multiple pathway scenarios but it certainly knocks the IfThenElse pattern into a cocked hat.

History

  • 6th May, 2020: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here