[utils] Add state machine implementation

This commit is contained in:
Mark Spanbroek 2022-09-27 15:01:17 +02:00 committed by Eric Mastro
parent e648c26340
commit b52d291785
3 changed files with 135 additions and 0 deletions

View File

@ -0,0 +1,86 @@
import pkg/questionable
import ./optionalcast
## Implementation of the the state pattern:
## https://en.wikipedia.org/wiki/State_pattern
##
## Define your own state machine and state types:
##
## type
## Light = ref object of StateMachine
## color: string
## LightState = ref object of State
##
## let light = Light(color: "yellow")
##
## Define the states:
##
## type
## On = ref object of LightState
## Off = ref object of LightState
##
## Perform actions on state entry and exit:
##
## method enter(state: On) =
## echo light.color, " light switched on"
##
## method exit(state: On) =
## echo light.color, " light no longer switched on"
##
## light.switch(On()) # prints: 'light switched on'
## light.switch(Off()) # prints: 'light no longer switched on'
##
## Allow behaviour to change based on the current state:
##
## method description*(state: LightState): string {.base.} =
## return "a light"
##
## method description*(state: On): string =
## if light =? (state.context as Light):
## return "a " & light.color & " light"
##
## method description*(state: Off): string =
## return "a dark light"
##
## proc description*(light: Light): string =
## if state =? (light.state as LightState):
## return state.description
##
## light.switch(On())
## echo light.description # prints: 'a yellow light'
## light.switch(Off())
## echo light.description # prints 'a dark light'
export questionable
export optionalcast
type
StateMachine* = ref object of RootObj
state: ?State
State* = ref object of RootObj
context: ?StateMachine
method enter(state: State) {.base.} =
discard
method exit(state: State) {.base.} =
discard
func state*(machine: StateMachine): ?State =
machine.state
func context*(state: State): ?StateMachine =
state.context
proc switch*(machine: StateMachine, newState: State) =
if state =? machine.state:
state.exit()
state.context = StateMachine.none
machine.state = newState.some
newState.context = machine.some
newState.enter()
proc switch*(oldState, newState: State) =
if context =? oldState.context:
context.switch(newState)

View File

@ -1,3 +1,4 @@
import ./utils/teststatemachine
import ./utils/testoptionalcast
{.warning[UnusedImport]: off.}

View File

@ -0,0 +1,48 @@
import std/unittest
import pkg/questionable
import codex/utils/statemachine
type
Light = ref object of StateMachine
On = ref object of State
Off = ref object of State
var enteredOn: bool
var exitedOn: bool
method enter(state: On) =
enteredOn = true
method exit(state: On) =
exitedOn = true
suite "state machines":
setup:
enteredOn = false
exitedOn = false
test "calls `enter` when entering state":
Light().switch(On())
check enteredOn
test "calls `exit` when exiting state":
let light = Light()
light.switch(On())
check not exitedOn
light.switch(Off())
check exitedOn
test "allows access to state machine from state":
let light = Light()
let on = On()
check not isSome on.context
light.switch(on)
check on.context == some StateMachine(light)
test "removes access to state machine when state exited":
let light = Light()
let on = On()
light.switch(on)
light.switch(Off())
check not isSome on.context