Skip to content

Instantly share code, notes, and snippets.

@egonelbre
Created August 23, 2012 15:27
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save egonelbre/3437746 to your computer and use it in GitHub Desktop.
Save egonelbre/3437746 to your computer and use it in GitHub Desktop.
State Machine
function Machine(first, $){
var cur = {}, next = $[first];
var self = {
go : function(to){ next = next ? next : ($[to] || $.undefined); },
trigger : function(event){ var t = cur.tx && cur.tx[event]; t && self.go(t); }
};
return function(){
if(next){
cur.exit && cur.exit.call(self);
cur = next; next = undefined;
self.__proto__ = cur.data;
cur.enter && cur.enter.call(self);
}
cur.fn && cur.fn.call(self);
};
};
var m = Machine("tick", {
undefined : {
fn : function(){
console.log("ERROR: NO STATE!");
this.trigger("recover");
},
tx : {
recover : "tick"
}
},
tick : {
enter : function(){
console.log("entered tick");
},
fn : function(){
console.log("tick");
this.go("tock");
}
},
tock : {
data : {
counter : 0
},
exit : function(){
this.counter += 1;
},
fn : function(){
this.counter += 1;
console.log("tock : " + this.counter);
if(this.counter > 5)
this.go("invalid");
if(this.counter > 3)
this.trigger("forward");
},
tx : {
forward : "tick"
}
}
});
console.log("=========================");
for(var i = 0; i < 15; i += 1)
m();
@zzzgit
Copy link

zzzgit commented Apr 11, 2022

Hey, bro, I couldn't understand the logic of your code even with debugging. It's harder to understand in a closure style. Do you have an article to explain it?

@egonelbre
Copy link
Author

I wouldn't recommend writing this convoluted code, but the short explanation is:

  1. $ in Machine keeps all the different states.
  2. line 7 returns a function that advances the state machine
  3. stepping the state machine involves several steps:
    3.1 check whether we need to go to a different state, if yes:
    3.1.1 then call the exit method on the state
    3.1.2 then change the state
    3.1.3 change the self data to point to the current state
    3.1.4. call the enter on the state
    3.2. call the state function

self acts the object for holding the state and different methods. The methods are go, which changes the state directly to another. The other one is trigger which looks up the event name from tx and then advances to that state.

Changing the data that this has is done via changing out the ._proto_ inheritance.

Here's a slightly simpler version of it:

function Machine(first, states){
    return {
        currentState: undefined,
        nextState: states[first],
        states: states,

        go(targetState){
            // the first `go` wins
            if(this.nextState !== undefined) {
                return
            }
            this.nextState = this.states[targetState];
            if(this.nextState === undefined) {
                 // fallback to an error state
                this.nextState = this.states.undefined;
            }
        },
        advance() {
            if(this.nextState) {
                this.currentState = this.nextState;
                this.nextState = undefined;
                this.__proto__ = this.currentState.data;
            }
            if(this.currentState.fn) {
                this.currentState.fn.call(this);
            }
        }
    }
};

var machine = Machine("tick", {
    tick : {
        fn : function(){
            console.log("tick");
            this.go("tock");
        }
    },
    tock : {
        data : {
            counter : 0
        },
        fn : function(){
            this.counter += 1;
            console.log("tock : " + this.counter);
            if(this.counter > 5)
                this.go("tick");
        }
    }
});

console.log("=========================");

for(var i = 0; i < 15; i += 1)
    machine.advance();

@zzzgit
Copy link

zzzgit commented Apr 26, 2022

Thanks, bro.

I tried to write a typesafe version which is written in Typescript.

This is the transpiled result:

var tsCustomError = require('ts-custom-error');

class StateMachine {
    _currentState;
    _states;
    _logic;
    constructor(states, initialState, logic) {
        this._states = states;
        this._currentState = initialState;
        this._logic = logic;
    }
    getAllStates() {
        return [...this._states];
    }
    getCurrentState() {
        return this._currentState;
    }
    _verifyState(value) {
        if (!this._states.includes(value)) {
            throw new tsCustomError.CustomError(`[StateMachine][_verifyState]: value "${value}" is not defined in initial states!`);
        }
    }
    switchBy(...factors) {
        const newState = this._logic(this._currentState, ...factors);
        this._verifyState(newState);
        this._currentState = newState;
        return this._currentState;
    }
}

exports.StateMachine = StateMachine;

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