Home

Awesome

go-fsm

GoDoc CircleCI

A scriptable FSM library for Go

Concepts

State Machines

A state machine is defined by

States

A state is defined by:

Transitions

A transition is defined by:

Condition Functions

If condition function name is not specified (an empty space), the transition is considered as unconditional (always evalutes to true).

Condition functions in the script should take 3 arguments:

func(src, dst, v) {
    /* some logic */
    return some_value 
}

The state machine use the returned value to determine the condition of the transition. E.g. condition is fulfilled if the value is truthy. In Tengo, the function that does not return anything is treated as if it returns undefined which is falsy.

Action Functions

Action functions in the script should take 3 arguments:

func(src, dst, v) {
    /* some logic */
    return some_value 
}

The data value passed to action functions is immutable, but, the function may return a new value to change the data value for the future condition/action functions.

Input and Output

When running the state machine, user can pass an input data value that will be used by condition and action functions. The state machine will return the final output data value when there are no more transitions available.

Execution Flow

  1. When the state machine starts, it's given an initial state and the input data.
  2. The state machine evaluates a list of transitions that are defined with the current state as its src state. The state machine evaluates the transitions in the same order they were added (defined).
    1. If condition script is specified, the state machine runs the script to determines whether the condition is fulfilled or not.
    2. If condition script is not specified,
  3. If one of the transition's condition is fulfilled, the state machine runs the action scripts:
    1. It runs exit action of the current state if it's defined.
    2. It runs action of the transition if it's defined.
    3. It runs entry action of the next state if it's defined.
  4. If no transitions were fulfilled, the state machine stops and returns the final value.
  5. Repeat from the step 2.

Example

Here's an example code for an FSM that tests if the input string is valid decimal numbers (e.g. 123.456) or not:

package main

import (
    "fmt"

    "github.com/d5/go-fsm"
)

var decimalsScript = []byte(`
fmt := import("fmt")

export {
	// test if the first character is a digit
	is_digit: func(src, dst, v) {
		return v[0] >= '0' && v[0] <= '9'
	},
	// test if the first character is a period
	is_dot: func(src, dst, v) {
		return v[0] == '.'  
	},
	// test if there are no more characters left
	is_eol: func(src, dst, v) {
		return len(v) == 0  
	},
	// prints out transition info
	print_tx: func(src, dst, v) {
		fmt.printf("%s -> %s: %q\n", src, dst, v)
	},
	// cut the first character
	enter: func(src, dst, v) {
		return v[1:]
	},
	enter_end: func(src, dst, v) {
		return "valid number"
	}, 
	enter_error: func(src, dst, v) {
		return "invalid number: " + v
	}
}`)

func main() {
    // build and compile state machine
    machine, err := fsm.New(decimalsScript).
        State("S", "enter", "").       // start
        State("N", "enter", "").       // whole numbers
        State("P", "enter", "").       // decimal point
        State("F", "enter", "").       // fractional part
        State("E", "enter_end", "").   // end
        State("X", "enter_error", ""). // error
        Transition("S", "E", "is_eol", "print_tx").
        Transition("S", "N", "is_digit", "print_tx").
        Transition("S", "X", "", "print_tx").
        Transition("N", "E", "is_eol", "print_tx").
        Transition("N", "N", "is_digit", "print_tx").
        Transition("N", "P", "is_dot", "print_tx").
        Transition("N", "X", "", "print_tx").
        Transition("P", "F", "is_digit", "print_tx").
        Transition("P", "X", "", "print_tx").
        Transition("F", "E", "is_eol", "print_tx").
        Transition("F", "F", "is_digit", "print_tx").
        Transition("F", "X", "", "print_tx").
        Compile()
    if err != nil {
        panic(err)
    }

    // test case 1: "123.456"
    res, err := machine.Run("S", "123.456")
    if err != nil {
        panic(err)
    }
    fmt.Println(res)

    // test case 2: "12.34.65"
    res, err = machine.Run("S", "12.34.56")
    if err != nil {
        panic(err)
    }
    fmt.Println(res)
}