fine

package module
v1.0.0-beta1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 16, 2021 License: MIT Imports: 4 Imported by: 0

README

fine

This is fine.

Go Reference Go Report Card

A Finite State Machine Go library, kept simple.

Note: the implementation is currently ongoing. The public API interface may change.

Installation

go get interrato.dev/fine

Concepts

States

A state is a description of the status of the depicted system. Every state has type string.

Transitions

A transition is an association between an event and an action. An event has always type string.

Note: a transition does not necessarily imply a change of the system state.

Actions

An action is the result of an event happening on the depicted system. An action can have one of the following types, or nil.

  • string
  • func() string
  • func(args ...interface{}) string
  • func()
  • func(args ...interface{})

When an action has one of the first three types, it causes a change of the system state.

Lifecycle actions

A lifecycle action is a special kind of action that runs automatically in specific instants of the system lifecycle. Lifecycle actions differ in which types they can have from normal ones. The following are the valid types for lifecycle actions.

  • func()
  • func(this *fine.FSM)
  • func(metadata fine.Metadata)
  • func(this *fine.FSM, metadata fine.Metadata)

Two lifecycle actions are available, characterized by the @enter and @exit events. These actions run when the system enters a new state and when the system leaves a state, respectively.

Metadata

The fine.Metadata type is simply a struct which contains the following information:

  • the From field with type string: the previous state from which the transition to a different state started;
  • the To field with type string: the new state where the transition will end;
  • the Event field with type string: the name of the action that caused the system state change;
  • the Args field with type []interface{}: the parameters that were passed to the action.

Code example

The following example shows the usage of the library in a simple situation. The considered system is a simple turnstile with a state diagram like this one.

package main

import (
    "fmt"

    "interrato.dev/fine"
)

func main() {
    turnstile := fine.Machine("locked", fine.States{
        "locked": {
            "pay":  "unlocked",
            "push": nil,
        },
        "unlocked": {
            "pay":  nil,
            "push": "locked",
        },
    })

    unsubscribe := turnstile.Subscribe(func(state string) {
        fmt.Printf("The turnstile is now %s.\n", state)
    })

    turnstile.Do("pay")
    turnstile.Do("push")

    unsubscribe()
}

The output of the previous code is as follows.

$ go run ./examples/turnstile
The turnstile is now locked.
The turnstile is now unlocked.
The turnstile is now locked.

For more code examples see the examples folder.

License

This project is licensed under the MIT License. See the LICENSE file for the full license text.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type FSM

type FSM struct {
	// contains filtered or unexported fields
}

FSM is a finite-state machine that can be instantiated using the Machine function.

func Machine

func Machine(initialState string, states States) *FSM

Machine instatiate a new FSM with the given initial state and the given set of possible states.

Note: the given initial state must be within the given possible states.

Example
package main

import (
	"fmt"

	"interrato.dev/fine"
)

func main() {
	powerSwitch := fine.Machine("off", fine.States{
		"off": {"toggle": "on"},
		"on":  {"toggle": "off"},
	})

	fmt.Println("Current FSM state:", powerSwitch.State())
}
Output:
Current FSM state: off

func (*FSM) Add

func (m *FSM) Add(state string, transitions Transitions) error

Add allows to add a new state with its associated transitions. If a state with the same name is already present in the FSM a non-nil error is returned.

Example
package main

import (
	"fmt"

	"interrato.dev/fine"
)

func main() {
	powerSwitch := fine.Machine("off", fine.States{
		"off": {"toggle": "on"},
	})

	// Here I add the "on" state.
	powerSwitch.Add("on", fine.Transitions{"toggle": "off"})

	// Trying to add the "off" state, which is already in the FSM, will fail.
	err := powerSwitch.Add("off", fine.Transitions{"smash": "broken"})
	fmt.Println("Adding \"off\" failed?", err != nil)

	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state:", powerSwitch.State())
}
Output:
Adding "off" failed? true
Current FSM state: on

func (*FSM) AddOrMerge

func (m *FSM) AddOrMerge(state string, transitions Transitions)

AddOrMerge allows to add a new state with its associated transitions. If a state with the same name is already present in the FSM, its transitions will be merged, keeping the newer ones in case of collisions.

Example
package main

import (
	"fmt"

	"interrato.dev/fine"
)

func main() {
	powerSwitch := fine.Machine("off", fine.States{
		"off": {"toggle": "on"},
	})

	// Here I add the "on" state. No difference with Add() here.
	powerSwitch.AddOrMerge("on", fine.Transitions{"toggle": "off"})

	// Here I try to add the "off" state, which is already in the FSM, and the
	// new transitions are merged to the old ones.
	powerSwitch.AddOrMerge("off", fine.Transitions{"smash": "broken"})

	// So I can still toggle it.
	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state:", powerSwitch.State())

	// And toggle it again...
	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state:", powerSwitch.State())

	// ...and now I smash it.
	powerSwitch.Do("smash")
	fmt.Println("Current FSM state:", powerSwitch.State())
}
Output:
Current FSM state: on
Current FSM state: off
Current FSM state: broken

func (*FSM) AddOrReplace

func (m *FSM) AddOrReplace(state string, transitions Transitions)

AddOrReplace allows to add a new state with its associated transitions. If a state with the same name is already present in the FSM, its transitions will be completely overwritten.

Example
package main

import (
	"fmt"

	"interrato.dev/fine"
)

func main() {
	powerSwitch := fine.Machine("off", fine.States{
		"off": {"toggle": "on"},
	})

	// Here I add the "on" state. No difference with Add() here.
	powerSwitch.AddOrReplace("on", fine.Transitions{"toggle": "off"})

	// Here I try to add the "off" state, but, because it's already in the FSM,
	// its transitions are now replaced.
	powerSwitch.AddOrReplace("off", fine.Transitions{"smash": "broken"})

	// The "toggle" event for the "off" state does not exist anymore, so
	// nothing changes even if I try to invoke "toggle" many times.
	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state:", powerSwitch.State())
	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state still", powerSwitch.State())
	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state is", powerSwitch.State(), "again")

	// So, let's break this switch.
	powerSwitch.Do("smash")
	fmt.Println("Current FSM state:", powerSwitch.State())
}
Output:
Current FSM state: off
Current FSM state still off
Current FSM state is off again
Current FSM state: broken

func (*FSM) Do

func (m *FSM) Do(action string, args ...interface{}) (string, error)

Do executes the specified action on the FSM from the current state.

The action parameter specifies the event, that is, the action name.

It is possible to pass arguments to the action. If the action isn't a function or does not accept any parameter, the arguments will be ignored.

Note: lifecycle actions cannot be manually executed.

Example
package main

import (
	"fmt"

	"interrato.dev/fine"
)

func main() {
	powerSwitch := fine.Machine("off", fine.States{
		"off": {"toggle": "on"},
		"on":  {"toggle": "off"},
	})

	fmt.Println("Current FSM state:", powerSwitch.State())
	fmt.Println("Toggling...")
	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state:", powerSwitch.State())
}
Output:
Current FSM state: off
Toggling...
Current FSM state: on
Example (Lifecycle)
package main

import (
	"fmt"

	"interrato.dev/fine"
)

func main() {
	powerSwitch := fine.Machine("off", fine.States{
		"off": {
			"@exit": func(metadata fine.Metadata) {
				fmt.Println("[INFO] from:", metadata.From)
				fmt.Println("[INFO] to:", metadata.To)
				fmt.Println("[INFO] event:", metadata.Event)
				fmt.Println("[INFO] args:", metadata.Args)
			},
			"toggle": func(args ...interface{}) string {
				message := args[0].(string)
				fmt.Printf("message: %q\n", message)
				return "on"
			},
		},
		"on": {
			"@enter": func() {
				fmt.Println("Finally, light!")
			},
			"toggle": "off",
		},
	})

	fmt.Println("Current FSM state:", powerSwitch.State())
	fmt.Println("Toggling...")
	powerSwitch.Do("toggle", "Shine, step into the light")
	fmt.Println("Current FSM state:", powerSwitch.State())
}
Output:
Current FSM state: off
Toggling...
message: "Shine, step into the light"
[INFO] from: off
[INFO] to: on
[INFO] event: toggle
[INFO] args: [Shine, step into the light]
Finally, light!
Current FSM state: on

func (*FSM) Exists

func (m *FSM) Exists(state string) bool

Exists returns whether the specified state is a possible state for the FSM.

func (*FSM) State

func (m *FSM) State() string

State returns the current state of the FSM.

func (*FSM) States

func (m *FSM) States() []string

States returns a slice with all the possible states of the FSM.

Note: the order is not guaranteed.

func (*FSM) Subscribe

func (m *FSM) Subscribe(callback func(state string)) func()

Subscribe allows subscribing to state changes with a callback function. The callback function will be executed every time the state changes and receives the new state as a parameter. The callback function also runs when subscribing and will receive the current state.

An unsubscribe function is returned.

Example
package main

import (
	"fmt"

	"interrato.dev/fine"
)

func main() {
	powerSwitch := fine.Machine("off", fine.States{
		"off": {"toggle": "on"},
		"on":  {"toggle": "off"},
	})

	onSubscribe := true
	unsubscribe := powerSwitch.Subscribe(func(state string) {
		if onSubscribe {
			fmt.Printf("Subscribed with state set to %q\n", state)
			onSubscribe = false
		} else {
			fmt.Printf("State just changed to %q\n", state)
		}
	})

	powerSwitch.Do("toggle")
	powerSwitch.Do("toggle")
	powerSwitch.Do("toggle")

	unsubscribe()

	powerSwitch.Do("toggle")
	fmt.Println("Current FSM state:", powerSwitch.State())
}
Output:
Subscribed with state set to "off"
State just changed to "on"
State just changed to "off"
State just changed to "on"
Current FSM state: off

type Metadata

type Metadata struct {
	// The previous state from which the transition started.
	From string

	// The new state where the transition will end.
	To string

	// The name of the action that caused the system state change.
	Event string

	// The arguments that were passed to the action.
	Args []interface{}
}

Metadata holds the information about a transition that changed the system state.

type States

type States map[string]Transitions

States are mappings from states to Transitions.

A state has type string.

type Transitions

type Transitions map[string]interface{}

Transitions is a mapping between events (or names of actions) and actions.

An action can have one of the following types, or nil.

string
func() string
func(args ...interface{}) string
func()
func(args ...interface{})

Trying to call an action that has a different type will panic.

There are two special lifecycle functions, named "@enter" and "@exit", executed on entering and exiting a state, respectively. It is not possible to pass custom parameters to these functions. They receive an optional Metadata object and an optional pointer to the FSM itself. Thus, the possible types for lifecycle actions are the following, or nil.

func()
func(this *fine.FSM)
func(metadata fine.Metadata)
func(this *fine.FSM, metadata fine.Metadata)

Directories

Path Synopsis
examples
blinking-led command
counter-switch command
simple-switch command
traffic-light command
turnstile command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL