A simple state machine in C#
Within the Abodit Natural Language engine there is often a need to track the state of various elements of a conversation. For example, is the user logged in or not, have they verified their email address, what instructional text have we offered them so far, ...
To make this easier I decided to add a simple state machine class to the Abodit utilities provided with my NLP Engine. There are, of course, a plethora of existing state machines on the web. Some of them are based on older .NET technology lacking use of generics and functional programming techniques. Others go overboard with fluent-style interfaces when a simple inheritance-based approach from an abstract base class would actually be simpler, less code, and more powerful. Most of them I didn't discover until after I'd built this one. In any case, it's always a good learning exercise to try to build something from scratch, so here goes ...
First let's take a look at the result. Here's how you can define a state machine that derives from this new StateMachine class:
[csharp] public class LoginOutStatemachine : StateMachine<LoginOutStatemachine> { public static void ReportEnter(LoginOutStatemachine m, Event e, State state) { Console.WriteLine(m.User + " entered state " + state + " via " + e); }
public static void ReportLeave(LoginOutStatemachine m, State state, Event e) { Console.WriteLine(m.User + " left state " + state + " via " + e); }
public static State Initial = new State("Initial", ReportEnter, ReportLeave); public static State LoggedIn = new State("Logged In", ReportEnter, ReportLeave); public static State LoggedOut = new State("Logged Out", ReportEnter, ReportLeave); public static State Deleted = new State("Deleted", ReportEnter, ReportLeave);
private static Event eLogsIn = new Event("Logs In"); private static Event eLogsOut = new Event("Logs Out"); private static Event eDeletesAccount = new Event("Account Deleted");
static LoginOutStatemachine() { Initial .When(eLogsIn, (m, s, e) => { Console.WriteLine("Logging in " + m.User); return LoggedIn; }) .When(eDeletesAccount, (m, s, e) => { Console.WriteLine("Deleting account " + m.User); return Deleted; }); LoggedIn .When(eLogsOut, (m, s, e) => { Console.WriteLine("Logging out " + m.User); return LoggedOut; }) .When(eDeletesAccount, (m, s, e) => { Console.WriteLine("Account deleted " + m.User); return Deleted; }); LoggedOut .When(eLogsIn, (m, s, e) => { Console.WriteLine("Logging in " + m.User); return LoggedIn; }) .When(eDeletesAccount, (m, s, e) => { Console.WriteLine("Account deleted " + m.User); return Deleted; }); }
public User User { get; private set; }
public LoginOutStatemachine(State initial, User user) : base(initial) { this.User = user; }
// Expose the events as public methods
public void LogsIn() { this.EventHappens(eLogsIn); }
public void LogsOut() { this.EventHappens(eLogsOut); }
public void DeletesAccount() { this.EventHappens(eDeletesAccount); } } [/csharp]
As you can see, you define the states and the events for the state machine using static definitions. (Events trigger state changes and associated actions). Typically I'll make the States public but the events private and instead provide method calls for each event that is allowed.
Each state can also have an Action that fires on entering the state and an action that fires on leaving the state and each action is provided with all of the parameters it might need (the state machine instance, the state it is going to or from, and the event that caused the transition to happen). In this case all of these entry and exit events are linked to the same method that simply reports what happened.
To define what happens when an given event is received by the state machine you create the static constructor as shown and then, using a fluent interface you define for each initial state, the transition to a new state by calling the When method passing it the event and the action to take when that event happens from the initial state specified. At the end of the method you must return the new state:
[csharp] Initial .When(eLogsIn, (m, s, e) => { Console.WriteLine("Logging in " + m.User); return LoggedIn; }) .When(eDeletesAccount, (m, s, e) => { Console.WriteLine("Deleting account " + m.User); return Deleted; }); [/csharp]
The (m, s, e) parameters give you the state machine itself, the state you are coming from and the event that has been received. By passing your method all of these values I make it easy for you to access any properties of the state machine itself (e.g. a User object) and also allow you to write a single method that handles more than one event type or more than one initial state but which can still be parameterized by those values.
The other minor trick is that the StateMachine class is a generic in the state machine class itself. A small trick that allows access to `T` as the type of the inherited state machine class and thus to any additional properties you define there.
Note how your state machine class can have properties like `User` which allows the transition code to access any additional data it needs. You create an instance of the state machine for each user (all the heavy lifting is done in the static definition so the state machine remains a light-weight object).
In the case of the NLP engine you can pass an `IListener` in to the state machine constructor also so that you can `Say` messages back to the user. Since the state machine is such a light-weight object you can afford to create it for each message interaction with the user and the information you need to persist is just the current state (which I will soon make into a string lookup).
If you want to use the actual state machine in any of your own projects (gratis), here's the current code:
[csharp] /// <summary> /// A state machine allows you to track state and to take actions when states change /// This state machine provides a fluent interface for defining states and transitions /// </summary> /// <remarks> /// Nasty generic of self so we can refer to the inheriting class in here /// </remarks> [Serializable] [DebuggerDisplay("Current State = {CurrentState.Name}")] public abstract class StateMachine<T> where T:StateMachine<T> { public State CurrentState { get; set; }
public StateMachine(State initial) { this.CurrentState = initial; }
/// <summary> /// An event has happened, transition to next state /// </summary> public void EventHappens(Event @event) { this.CurrentState = this.CurrentState.OnEvent((T)this, @event); }
/// <summary> /// An event that causes the state machine to transition to a new state /// </summary> /// <remarks> /// Defined as a nested class so that this state machine's events can only be used with it /// </remarks> [DebuggerDisplay("Event = {Name}")] public class Event { public string Name { get; private set; } public Event(string name) { this.Name = name; } public override string ToString() { return "~" + this.Name + "~"; } }
/// <summary> /// A state that the state machine can be in /// </summary> /// <remarks> /// Defined as a nested class so that this state machine's states can only be used with it /// </remarks> [DebuggerDisplay("State = {Name}")] public class State { /// <summary> /// The Name of this state /// </summary> public string Name { get; private set; }
public Action<T, State, Event> ExitAction { get; private set; } public Action<T, Event, State> EntryAction { get; private set; }
private readonly IDictionary<Event, Func<T, State, Event, State>> transitions = new Dictionary<Event, Func<T, State, Event, State>>();
/// <summary> /// Create a new State with a name and an optional entry and exit action /// </summary> public State(string name, Action<T, Event, State> entryAction = null, Action<T, State, Event> exitAction = null) { this.Name = name; this.EntryAction = entryAction; this.ExitAction = exitAction; }
public State When(Event @event, Func<T, State, Event, State> action) { transitions.Add(@event, action); return this; }
public State OnEvent(T parent, Event @event) { Func<T, State, Event, State> transition = null; if (transitions.TryGetValue(@event, out transition)) { State newState = transition(parent, this, @event); if (newState != this) { // Entry and exit actions only fire when CHANGING state if (this.ExitAction != null) this.ExitAction(parent, this, @event); if (newState.EntryAction != null) newState.EntryAction(parent, @event, newState); } return newState; } else return this; // did not change state }
public override string ToString() { return "*" + this.Name + "*"; } } } } [/csharp]
For further reading on State Machines I recommend this Wikipedia Article.