patternkotlinModerate
Event-driven finite state machine DSL in Kotlin
Viewed 0 times
finitekotlindslstatemachinedrivenevent
Problem
I wrote a DSL for defining a finite state machine. My robotics team wants to use a state machine to manage the state of the subsystems on our robot, like the arms, drive train, lifter, etc. Each subsystem is implemented as a FSM and subscribes to events published by buttons and sensors. I'm using an event bus as the middleman.
The design takes inspiration from Akka's FSM actor (written in Scala). JetBrain's Anko and Spek wonderfully showcase Kotlin's DSL capabilities, but outside of these libraries there are surprisingly few resources on how one would go about writing a DSL.
Thus, while this code works, it feels a clunky and I can't help but think I've missed something. I've explicitly listed some of the smells below, but I appreciate advice on all fronts.
Design goals and concerns
As far as state machines go, this would be m
The design takes inspiration from Akka's FSM actor (written in Scala). JetBrain's Anko and Spek wonderfully showcase Kotlin's DSL capabilities, but outside of these libraries there are surprisingly few resources on how one would go about writing a DSL.
Thus, while this code works, it feels a clunky and I can't help but think I've missed something. I've explicitly listed some of the smells below, but I appreciate advice on all fronts.
Design goals and concerns
- Scoping. The appeal of a DSL lies in its restrictions. However, with the current structure we're leaking the
buildmethod. Although harmless, it should never be called while in the DSL. Is there any way to not expose this in the DSL or is this something we just have to accept as unavoidable?
- On this note, do extension functions help here?
- Transitions. Transitions boil down to a function of
(S, E) -> S(with side effects). In the DSL it's exposed as thewhenStateparameter forTransition.(E) -> Intentwhere Intent is a sealed class with only the options forGotoorStay. The builder then maps that into a transition function. Can we change the design to expose only thegotoandstayhelper functions inTransitionand keep private their respectiveIntentconstructors?
- Event-driven. Not related to the DSL, but I went so far as to make
StateMachinedirectly implement(E) -> Unitin order to have a shared interface with my event bus; now I can just pass in a direct instance of the state machine as a listener instead of a method reference. I'm surprised I can do this. Although it's awesome, is this bad practice?
As far as state machines go, this would be m
Solution
I'd design the StateMachine differently.
The StateMachine does not need to know about all the possible Transitions between the States. All the StateMachine should care about is the current State it is in.
The State itself should know about all possible Transitions (I call them Edges).
So if an Event occurs, the StateMachine asks the current State, whether there is an appropriate Edge to go down. This Edge then tells the StateMachine what the current State of the StateMachine is after the Transition.
Just like the Strategy-Pattern: Wikipedia: Strategy Pattern
I've created a little Proof-of-Concept:
There is the class State:
It contains all of the possible Transitions(Edges). Furthermore, every State stores some actions, which will be called upon entering the State. (e.g. set motor-speed, stop motor)
Here is the Edge Class:
Every Edge has actions too, which will be called when you transition to another State. Also the Edge returns the new State after the transition is done.
And then there is the StateMachine with its Building-Function:
The different Events that occur:
Now you have your own small DSL to Build a complete StateMachine:
```
fun main(args: Array) {
val stateMachine = buildStateMachine("Idle") {
state(name="Idle") {
action {
println("Entered state ${it.name}")
}
action {
println("Set motor-speed to 0")
}
edge(name = "Go Up", targetState = "Going Up") {
eventHandler = { it is Event.GoUpEvent }
}
edge(name = "Go Down", targetState = "Going Down") {
eventHandler = { it is Event.GoDownEvent }
}
}
state(name="Going Up") {
action {
println("Entered state ${it.name}")
println("Set motor-speed to 1.0")
}
edge(name="Top Limit Hit", targetState = "At Top") {
eventHandler = { it is Event.TopLimitHitEvent }
}
edge(name="Halt", targetState = "Idle") {
eventHandler = { it is Event.HaltEvent }
}
}
state(name="Going Down") {
action {
println("Entered state ${it.name}")
}
action {
println("Set motor-speed to -1.0")
}
edge(name="Bottom Limit Hit", tar
The StateMachine does not need to know about all the possible Transitions between the States. All the StateMachine should care about is the current State it is in.
The State itself should know about all possible Transitions (I call them Edges).
So if an Event occurs, the StateMachine asks the current State, whether there is an appropriate Edge to go down. This Edge then tells the StateMachine what the current State of the StateMachine is after the Transition.
Just like the Strategy-Pattern: Wikipedia: Strategy Pattern
I've created a little Proof-of-Concept:
There is the class State:
class State(val name: String) {
private val edgeList = mutableListOf()
fun edge(name: String, targetState: String, init: Edge.() -> Unit) {
val edge = Edge(name, targetState)
edge.init()
edgeList.add(edge)
}
private val stateEnterAction = mutableListOf Unit>()
//Add an action which will be called when the state is entered
fun action(action: (State) -> Unit) {
stateEnterAction.add(action)
}
fun enterState() {
stateEnterAction.forEach { it(this) }
}
//Get the appropriate Edge for the Event
fun getEdgeForEvent(event: Event): Edge {
return edgeList.first { it.canHandleEvent(event) }
}
}It contains all of the possible Transitions(Edges). Furthermore, every State stores some actions, which will be called upon entering the State. (e.g. set motor-speed, stop motor)
Here is the Edge Class:
class Edge(val name: String, val targetState: String) {
lateinit var eventHandler: (Event) -> Boolean
private val actionList = mutableListOf Unit>()
fun action(action: (Edge) -> Unit) {
actionList.add(action)
}
//Invoke when you go down the edge to another state
fun enterEdge(retrieveState: (String) -> State): State {
actionList.forEach { it(this) }
return retrieveState(targetState)
}
fun canHandleEvent(event: Event): Boolean {
return eventHandler(event)
}
}Every Edge has actions too, which will be called when you transition to another State. Also the Edge returns the new State after the transition is done.
And then there is the StateMachine with its Building-Function:
class StateMachine(private val initialStateName: String) {
private lateinit var currentState: State
private val stateList = mutableListOf()
fun state(name: String, init: State.() -> Unit) {
val state = State(name)
state.init()
stateList.add(state)
}
fun getStateByName(name: String): State {
val result = stateList.firstOrNull { it.name == name}
?: throw NoSuchElementException(name)
return result
}
fun initialize() {
currentState = getStateByName(initialStateName)
currentState.enterState()
}
fun eventOccured(event: Event) {
val edge = currentState.getEdgeForEvent(event)
if(edge is Edge) {
val newState = edge.enterEdge { getStateByName(it) }
newState.enterState()
currentState = newState
}
}
}
fun buildStateMachine(initialStateName: String, init: StateMachine.() -> Unit): StateMachine {
val stateMachine = StateMachine(initialStateName)
stateMachine.init()
return stateMachine
}The different Events that occur:
sealed class Event {
class GoUpEvent: Event() { }
class GoDownEvent: Event() { }
class HaltEvent: Event() { }
class TopLimitHitEvent: Event() { }
class BottomLimitHitEvent: Event() { }
}Now you have your own small DSL to Build a complete StateMachine:
```
fun main(args: Array) {
val stateMachine = buildStateMachine("Idle") {
state(name="Idle") {
action {
println("Entered state ${it.name}")
}
action {
println("Set motor-speed to 0")
}
edge(name = "Go Up", targetState = "Going Up") {
eventHandler = { it is Event.GoUpEvent }
}
edge(name = "Go Down", targetState = "Going Down") {
eventHandler = { it is Event.GoDownEvent }
}
}
state(name="Going Up") {
action {
println("Entered state ${it.name}")
println("Set motor-speed to 1.0")
}
edge(name="Top Limit Hit", targetState = "At Top") {
eventHandler = { it is Event.TopLimitHitEvent }
}
edge(name="Halt", targetState = "Idle") {
eventHandler = { it is Event.HaltEvent }
}
}
state(name="Going Down") {
action {
println("Entered state ${it.name}")
}
action {
println("Set motor-speed to -1.0")
}
edge(name="Bottom Limit Hit", tar
Code Snippets
class State(val name: String) {
private val edgeList = mutableListOf<Edge>()
fun edge(name: String, targetState: String, init: Edge.() -> Unit) {
val edge = Edge(name, targetState)
edge.init()
edgeList.add(edge)
}
private val stateEnterAction = mutableListOf<(State) -> Unit>()
//Add an action which will be called when the state is entered
fun action(action: (State) -> Unit) {
stateEnterAction.add(action)
}
fun enterState() {
stateEnterAction.forEach { it(this) }
}
//Get the appropriate Edge for the Event
fun getEdgeForEvent(event: Event): Edge {
return edgeList.first { it.canHandleEvent(event) }
}
}class Edge(val name: String, val targetState: String) {
lateinit var eventHandler: (Event) -> Boolean
private val actionList = mutableListOf<(Edge) -> Unit>()
fun action(action: (Edge) -> Unit) {
actionList.add(action)
}
//Invoke when you go down the edge to another state
fun enterEdge(retrieveState: (String) -> State): State {
actionList.forEach { it(this) }
return retrieveState(targetState)
}
fun canHandleEvent(event: Event): Boolean {
return eventHandler(event)
}
}class StateMachine(private val initialStateName: String) {
private lateinit var currentState: State
private val stateList = mutableListOf<State>()
fun state(name: String, init: State.() -> Unit) {
val state = State(name)
state.init()
stateList.add(state)
}
fun getStateByName(name: String): State {
val result = stateList.firstOrNull { it.name == name}
?: throw NoSuchElementException(name)
return result
}
fun initialize() {
currentState = getStateByName(initialStateName)
currentState.enterState()
}
fun eventOccured(event: Event) {
val edge = currentState.getEdgeForEvent(event)
if(edge is Edge) {
val newState = edge.enterEdge { getStateByName(it) }
newState.enterState()
currentState = newState
}
}
}
fun buildStateMachine(initialStateName: String, init: StateMachine.() -> Unit): StateMachine {
val stateMachine = StateMachine(initialStateName)
stateMachine.init()
return stateMachine
}sealed class Event {
class GoUpEvent: Event() { }
class GoDownEvent: Event() { }
class HaltEvent: Event() { }
class TopLimitHitEvent: Event() { }
class BottomLimitHitEvent: Event() { }
}fun main(args: Array<String>) {
val stateMachine = buildStateMachine("Idle") {
state(name="Idle") {
action {
println("Entered state ${it.name}")
}
action {
println("Set motor-speed to 0")
}
edge(name = "Go Up", targetState = "Going Up") {
eventHandler = { it is Event.GoUpEvent }
}
edge(name = "Go Down", targetState = "Going Down") {
eventHandler = { it is Event.GoDownEvent }
}
}
state(name="Going Up") {
action {
println("Entered state ${it.name}")
println("Set motor-speed to 1.0")
}
edge(name="Top Limit Hit", targetState = "At Top") {
eventHandler = { it is Event.TopLimitHitEvent }
}
edge(name="Halt", targetState = "Idle") {
eventHandler = { it is Event.HaltEvent }
}
}
state(name="Going Down") {
action {
println("Entered state ${it.name}")
}
action {
println("Set motor-speed to -1.0")
}
edge(name="Bottom Limit Hit", targetState = "At Bottom") {
eventHandler = { it is Event.BottomLimitHitEvent }
}
edge(name="Halt", targetState = "Idle") {
eventHandler = { it is Event.HaltEvent }
}
}
state(name="At Top") {
action {
println("Entered state ${it.name}")
println("Set motor-speed to 0")
}
edge(name = "Go Down", targetState = "Going Down") {
eventHandler = { it is Event.GoDownEvent }
}
}
state(name="At Bottom") {
action {
println("Entered state ${it.name}")
println("Set motor-speed to 0")
}
edge(name = "Go Up", targetState = "Going Up") {
eventHandler = { it is Event.GoUpEvent }
}
}
}
stateMachine.initialize()
stateMachine.eventOccured(Event.GoUpEvent())
stateMachine.eventOccured(Event.TopLimitHitEvent())
stateMachine.eventOccured(Event.GoDownEvent())
stateMachine.eventOccured(Event.BottomLimitHitEvent())
stateMachine.eventOccured(Event.GoUpEvent())
stateMachine.eventOccured(Event.HaltEvent())
}Context
StackExchange Code Review Q#143726, answer score: 10
Revisions (0)
No revisions yet.