I recently completed the development of a suite of tools for my client-only Angular Dev Toolkit. This development was centered around the concept of data-driven logic in applications. Two of the tools (Expression-based Decision Tree and Sequencing Engine) were derivatives of work for NDA clients, so they remain proprietary to the toolkit.
Based on private and social-media conversations, it appears there is sufficient interest to warrant open-sourcing the baseline version of the finite state machine, which is the final component of this development.
So, here it is, and always remember the three rules
1 - Experiment 2 - Have Fun 3 - Drink Coffee
Author: Jim Armstrong - The Algorithmist
@algorithmist
theAlgorithmist [at] gmail [dot] com
Typescript: 2.4.3
Rxjs: 5.5.6
Requires ES6 Map and Set
Version: 1.0
NOTE: This repo is obsolete. The FSM has been upgraded and is now a part of the AMYR library
Installation involves all the usual suspects
- npm and gulp installed globally
- Clone the repository
- npm install
- get coffee (this is the most important step)
-
gulp compile
-
gulp test
The test suite is in Mocha/Chai and specs reside in the test folder.
This page contains one of my favorite online descriptions of a Finite State Machine and is closest to how I learned the topic 'back in the day.'
The machine implementation in this repo is Mealy-style, although a state may be passed as data, so Moore-style machines can be used within this framework.
A FSM consists of a number of named states, and transitions from those states. One or more states may be defined as an acceptance state, meaning that after exercising the machine with a finite number of inputs, the machine may indicate acceptance of a certain criteria. This could be a valid string or that the number of zeros in a binary sequence is even. It is possible (and I've done this in C++) to define a rejection state with the convention that the machine may never transition out of this state. This is a way to catch and flag incorrect or other inputs.
The set of inputs to a FSM is called an alphabet.
The FiniteStateMachine class in this distribution has two possibly differentiating features from other implementations (particularly in Java, JS, and TS). One is that listening for specific state transitions is not performed by callback functions applied to those states. Instead, the machine is reactive. One or more (RxJs) Observers may subscribe to the machine and receive updates on all transitions that includes from and to states as well as relevant data.
The FiniteStateMachine class exposes the following models
// this is normally part of the Decision Tree library, but has been ripped out to make this distribution standalone
export interface IDecisionTreeAction
{
success: boolean; // true if operation was successful
node?: Object; // reference to the data Object in which an error was detected
action: string; // action to take
}
/**
* A state transition must have a 'from' and 'to' (named) state and may contain optional Object data
*/
export interface IStateTransition
{
from: string;
to: string;
data?: Object;
}
/**
* Output from a state transition is the 'to' state and optional data obtained from the transition function
*/
export interface IStateOutput
{
to: string;
data?: any;
}
/**
* The transition function is Mealy-style, that is a transition to a new state is based on prior state and
* input data. Since state is optional in this interface, pass the state name as data and a Moore-style
* machine can be implemented.
*/
export interface transFunction
{
(data: any, state?: string): IStateOutput;
}
The public API of the class is as follows.
public static create(data: Object, name?: string): FiniteStateMachine | null
public get numStates(): number
public get numTransitions(): number
public get currentState(): string
public get states(): IterableIterator<string>
public get initialState(): string
public get initialData(): Object | null
public get isAcceptance(): boolean
public get alphabet(): Array<string> | null
public fromJson(data: Object): IDecisionTreeAction
public addState(stateName: string, acceptance: boolean=false): void
public addTransition(from: string, to: transFunction): boolean
public addSubscriber(observer: Observer <IStateTransition>): void
public next(input: any, initialState?: string): IStateOutput | null
public clear(): void
The FSM in this distribution may be used by directly assigning states and transition functions. The machine may also be described with Object data. These cases are best illustrated by example.
First, consider one of the machines discussed in the above introductions to FSM's. This machine is designed to test sequences consisting of the letters a, b, c, and d, which is the machine's alphabet.
The states are S1, S2, S3, and S4, the latter of which is the acceptance state. The machine's initial state is S1 and letters are input one at a time. The machine is tested for being in the acceptance state after the final transition (refer to the above link for the state diagram).
This machine can be implemented with the following code (which is provided in the specs located in the test folder) which presumes a string input Array, str, i.e.
const str: Array<string> = ['a', 'a', 'a', 'a', 'c', 'd'];
Note that the transition functions are defined statically in code, which allows them to use stronger typing than is described in the transFunction interface.
For compactness, testing the return value from adding a transition is not included in the example.
const f12: transFunction = (data: string) => {
return data == 'a' ? {to: 'S2'} : {to: 'S1'}
};
const f22: transFunction = (data: string) => {
return data == 'a' ? {to: 'S2'} : (data == 'b' ? {to: 'S1'} : (data == 'c' ? {to: 'S4'} : {to: 'S2'}))
};
const f32: transFunction = (data: string) => {
return data == 'a' ? {to: 'S1'} : (data == 'b' ? {to: 'S4'} : {to: 'S3'})
};
const f42: transFunction = (data: string) => {
return data == 'd' ? {to: 'S3'} : {to: 'S4'}
};
.
.
.
const __machine: FiniteStateMachine = new FiniteStateMachine();
__machine.addState('S1');
__machine.addState('S2');
__machine.addState('S3');
__machine.addState('S4', true);
__machine.addTransition('S1', f12);
__machine.addTransition('S2', f22);
__machine.addTransition('S3', f32);
__machine.addTransition('S4', f42);
const n: number = str.length;
let i: number;
let state: IStateOutput = __machine.next(str[0], 'S1'); // set the initial state and first input
for (i = 1; i < n; ++i) {
state = __machine.next(str[i]);
}
Based on the machine's construction, we would expect __machine.isAcceptance to be false. This is also the answer to the quiz question posed on the site from which the example was taken :)
Many algorithms can be expressed as a FSM and Regex is equivalent to FSM (Kleene's Theorem), for example. Whether or not one should implement an algorithm using a FSM is another topic. One example that often does not seem to fit a FSM architecture is the 'change machine' problem (this was a lab exercise in college).
Consider a machine that accepts coins (penny, nickel, dime, quarter) for an amount less than a dollar. After each coin is deposited, the machine updates the remaining balance, indicates if sufficient payment has been made, and computes any necessary change.
Alphabet and machine states may be considered equivalent in this machine, so we define states p, n, d, q, and c for the coin denominations and a 'complete' state. There is no transition out of the complete state, which is also the acceptance state for this machine.
Code to implement this machine is provided in the specs.
The most potentially useful feature of this FSM is the ability to describe a machine in data. This allows multiple machines to be defined in metadata and then dynamically applied based on real-time conditions in an application.
The string-test machine shown above can be described in the following data Object
const machine1: Object = {
name: 'StringTest',
initialState: "S1",
alphabet: [
'a',
'b',
'c',
'd'
],
states: [
{
name: 'S1',
isAcceptance: false,
transition: "return data == 'a' ? {to: 'S2'} : {to: 'S1'}"
},
{
name: 'S2',
isAcceptance: false,
transition: "return data == 'a' ? {to: 'S2'} : (data == 'b' ? {to: 'S1'} : (data == 'c' ? {to: 'S4'} : {to: 'S2'}))"
},
{
name: 'S3',
isAcceptance: false,
transition: "return data == 'a' ? {to: 'S1'} : (data == 'b' ? {to: 'S4'} : {to: 'S3'})"
},
{
name: 'S4',
isAcceptance: true,
transition: "return data == 'd' ? {to: 'S3'} : {to: 'S4'}"
},
]
};
Transition functions defined in data will be called with arguments that strictly match the transFunction interface. They must adhere to the following conventions.
- Only the function body need be defined in a string.
- The variable names data and state are arguments. Use them as such.
- Transition functions must be pure.
- The self-referential pointer (this) is bound to the global context due to Function constructor invocation. Do not use this inside the function body.
- Do not reference any variables that are not defined in the function body as we can not be 'loose' regarding execution context of these functions.
In a typical application, the above Object description is input external to the application. Implementation of the machine reduces to
let result: IDecisionTreeAction = __machine.fromJson(machine1);
let str: Array<string> = ['a', 'b', 'a', 'c', 'd', 'a', 'a', 'c'];
let n: number = str.length;
let i: number;
let state: IStateOutput;
for (i = 0; i < n; ++i) {
state = __machine.next(str[i]);
}
We would expect __machine.isAcceptance to be true.
Refer to the specs in the test folder for more usage examples. This includes a data-driven version of the change machine example.
I will continue to modify this class for specific use in my Angular Dev Toolkit and hope you find this baseline version useful.
Apache 2.0
Free Software? Yeah, Homey plays that