Skip to content

Instantly share code, notes, and snippets.

Created August 13, 2013 04:07
Show Gist options
  • Save anonymous/6217808 to your computer and use it in GitHub Desktop.
Save anonymous/6217808 to your computer and use it in GitHub Desktop.
State machine in C# that I'm toying with. Inspired by https://github.com/soveran/micromachine but for C#
StateMachine car =
StateMachine.Create("off")
.Transition(when: "ignition", off: "park")
.Transition(when: "shift_up", park: "reverse", reverse: "neutral", neutral: "drive")
.Transition(when: "shift_down", drive: "neutral", neutral: "reverse", reverse: "park");
car.On("drive", () => Console.Log("Hit the road, Jack!"));
car.Any(() => Console.Log("Shifted into " + car.State));
car.Trigger("ignition"); // P
car.Trigger("shift_up"); // R
car.Trigger("shift_up"); // N
car.Trigger("shift_up"); // D
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
namespace StateManagement
{
public class StateMachine : DynamicObject
{
#region Private Fields
private const string SYNTAX =
"The syntax for Transition calls is machine.Transition(when: \"confirm\", pending: \"confirmed\");";
private readonly Dictionary<object, Dictionary<string, string>> transitionsFor = new Dictionary<object, Dictionary<string, string>>();
private readonly Dictionary<string, List<Action>> callbacks = new Dictionary<string, List<Action>>();
#endregion
#region Constructor
public StateMachine(string initial)
{
State = initial;
}
#endregion
#region Public Properties
/// <summary>
/// The current state of the machine.
/// </summary>
public string State { get; private set; }
/// <summary>
/// Returns an array of events registered with this machine.
/// </summary>
public object[] Events
{
get { return transitionsFor.Keys.ToArray(); }
}
/// <summary>
/// Returns an array of the accepted states for this machine.
/// </summary>
public string[] States
{
get { return transitionsFor.SelectMany(x => x.Value, (outer, inner) => inner.Key).Distinct().ToArray(); }
}
#endregion
#region Public Methods
/// <summary>
/// Allows for the dynamic binding of methods.
/// In this instance, the key method is "Transition"
/// which uses the parameter names as the states
/// to transition from and the value of the parameter
/// as the state to transition to. Also allows many
/// transitions to be bound at once.
/// </summary>
/// <example>
/// machine.Transition(when: "start", pending: "ready")
/// </example>
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
switch (binder.Name)
{
case "Transition":
var index = binder.CallInfo.ArgumentNames.IndexOf("when");
if (index == -1)
throw new ArgumentException("Missing required argument \"when\". " + SYNTAX);
var trigger = args[index];
if (binder.CallInfo.ArgumentCount < 2)
throw new ArgumentException("Needs to have at least one transition argument. " + SYNTAX);
if (binder.CallInfo.ArgumentNames.Count != binder.CallInfo.ArgumentCount)
throw new ArgumentException("All arguments must be named. " + SYNTAX);
for (int i = 0; i < args.Length; i++)
{
if (i == index)
continue;
var from = binder.CallInfo.ArgumentNames[i];
var to = (string) args[i];
GetEventStates(trigger).Add(from, to);
}
result = this;
return true;
default:
return base.TryInvokeMember(binder, args, out result);
}
}
/// <summary>
/// A non-dynamic version of the Transition method in case you're a hater.
/// </summary>
public StateMachine Transition(string when, string from, string to)
{
GetEventStates(when).Add(from, to);
return this;
}
/// <summary>
/// Affixes a callback to the machine when it enters the given state.
/// </summary>
public StateMachine On(string state, Action callback)
{
GetCallbacks(state).Add(callback);
return this;
}
/// <summary>
/// Affixes a callback to the machine every time it changes state.
/// </summary>
/// <param name="callback"></param>
/// <returns></returns>
public StateMachine Any(Action callback)
{
GetCallbacks("any").Add(callback);
return this;
}
/// <summary>
/// Returns whether triggering the event will cause the machine to change state.
/// </summary>
public bool Check(object trigger)
{
if (!transitionsFor.ContainsKey(trigger))
throw new InvalidEventException(trigger);
return transitionsFor[trigger].ContainsKey(State) || transitionsFor[trigger].ContainsKey("any");
}
/// <summary>
/// Triggers an event on the machine, possibly changing its state and invoking callbacks.
/// </summary>
public bool Trigger(object trigger)
{
if (!Check(trigger))
return false;
State = GetTransition(trigger);
var cb = GetTriggerCallbacks(State);
foreach (var action in cb)
action.Invoke();
return true;
}
#endregion
#region Static Methods
/// <summary>
/// Just a quick wrapper around the constructor
/// that returns the StateMachine as a dynamic
/// for nice, fluent use by the calling code.
/// </summary>
public static dynamic Create(string initial)
{
return new StateMachine(initial);
}
#endregion
#region Private Methods
private string GetTransition(object e)
{
var t = transitionsFor[e];
return t.ContainsKey(State) ? t[State] : t["any"];
}
private IEnumerable<Action> GetTriggerCallbacks(string state)
{
foreach (var callback in GetCallbacks(state))
yield return callback;
foreach (var callback in GetCallbacks("any"))
yield return callback;
}
private List<Action> GetCallbacks(string state)
{
if (callbacks.ContainsKey(state))
return callbacks[state];
var newCallback = new List<Action>();
callbacks.Add(state, newCallback);
return newCallback;
}
private Dictionary<string, string> GetEventStates(object trigger)
{
if (transitionsFor.ContainsKey(trigger))
return transitionsFor[trigger];
var states = new Dictionary<string, string>();
transitionsFor.Add(trigger, states);
return states;
}
#endregion
}
}
@MrLeebo
Copy link

MrLeebo commented Aug 13, 2013

Probably should have been logged in when I uploaded this ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment