kFSM is Finite State Machinery for Kotlin.
There are four key components to building your state machine.
- The nodes representing different states in the machine -
State
- The type to be transitioned through the machine -
Value
- The effects that are defined by transitioning from one state to the next -
Transition
- A transitioner, which can be customised when you need to define pre and post transition hooks -
Transitioner
Let's build a state machine for a traffic light.
stateDiagram-v2
[*] --> Green
Amber --> Red
Green --> Amber
Red --> Green
The states are a collection of related classes that define a distinct state that the value can be in. They also define which states are valid next states.
sealed class Color(to: () -> Set<Color>) : app.cash.kfsm.State<Color>(to)
data object Green : Color({ setOf(Amber) })
data object Amber : Color({ setOf(Red) })
data object Red : Color({ setOf(Green) })
Important
Be sure to define your state constructor with functions rather than literal values if you require cycles in your state machine. Otherwise, you are likely to encounter null pointer exceptions from the Kotlin runtime's inability to define the types.
The value is responsible for knowing and updating its current state.
data class Light(override val state: Color) : Value<Light, Color> {
override fun update(newState: Color): Light = this.copy(state = newState)
}
Types that provide the required side-effects that define a transition in the machine.
abstract class ColorChange(
from: States<Color>,
to: Color
) : Transition<Light, Color>(from, to) {
// Convenience constructor for when the from set has only one value
constructor(from: Color, to: Color) : this(States(from), to)
}
class Go(private val camera: Camera) : ColorChange(from = Red, to = Green) {
override suspend fun effect(value: Light) = camera.disable()
}
object Slow : ColorChange(from = Green, to = Amber)
class Stop(private val camera: Camera) : ColorChange(from = Amber, to = Red) {
override suspend fun effect(value: Light) = camera.enable()
}
Moving a value from one state to another is done by the transitioner. We provide it with a function that declares how to persist values.
class LightTransitioner(
private val database: Database
) : Transitioner<ColorChange, Light, Color>() {
override suspend fun persist(value: Light, change: ColorChange): Result<Light> = database.update(value)
}
Each time a transition is successful, the persist function will be called.
It is sometimes necessary to execute effects before and after a transition. These can be defined on the transitioner.
class LightTransitioner ... {
// ...
override suspend fun preHook(value: V, via: T): Result<Unit> = runCatching {
globalLock.lock(value)
}
override suspend fun postHook(from: S, value: V, via: T): Result<Unit> = runCatching {
globalLock.unlock(value)
notificationService.send(via.successNotifications())
}
}
With the state machine and transitioner defined, we can progress any value through the machine by using the transitioner.
val transitioner = LightTransitioner(database)
val greenLight: Result<Light> = transitioner.transition(redLight, Go)
// The state
sealed class Color(to: () -> Set<Color>) : app.cash.kfsm.State<Color>(to)
data object Green : Color({ setOf(Amber) })
data object Amber : Color({ setOf(Red) })
data object Red : Color({ setOf(Green) })
// The value
data class Light(override val state: Color) : Value<Light, Color> {
override fun update(newState: Color): Light = this.copy(state = newState)
}
// The transitions
abstract class ColorChange(
from: States<Color>,
to: Color
) : Transition<Light, Color>(from, to) {
// Convenience constructor for when the from set has only one value
constructor(from: Color, to: Color) : this(States(from), to)
}
class Go(private val camera: Camera) : ColorChange(from = Red, to = Green) {
override suspend fun effect(value: Light) = camera.disable()
}
object Slow : ColorChange(from = Green, to = Amber)
class Stop(private val camera: Camera) : ColorChange(from = Amber, to = Red) {
override suspend fun effect(value: Light) = camera.enable()
}
// The transitioner
class LightTransitioner(
private val database: Database
) : Transitioner<ColorChange, Light, Color>() {
override suspend fun persist(value: Light, change: ColorChange): Result<Light> = database.update(value)
}
// main ...
val transitioner = LightTransitioner(database)
val greenLight: Result<Light> = transitioner.transition(redLight, Go)
See lib/src/test/kotlin/app/cash/kfsm/exemplar for a different example of how to use this library.
If you are using coroutines and need suspending function support, you can extend TransitionerAsync
instead of
Transitioner
and implement any suspending transition effects via the Transition.effectAsync
method.
How does kFSM help validate the correctness of your state machine and your values?
- It is impossible to define a Transition that does not comply with the transitions defined in the States. For example,
a transition that attempts to define an arrow between
Red
andAmber
will fail at construction. - If a value has already transitioned to the target state, then a subsequent request will not execute the transition a
second time. The result will be success. I.e. it is a no-op.
- (unless you have defined a circular/self-transition, in which case it will)
- If a value is in a state unrelated to the executed transition, then the result will be an error and no effect will be executed.
The utility StateMachine.verify
will assert that a defined state machine is valid - i.e. that all states are visited
from a given starting state.
StateMachine.verify(Green) shouldBeRight true
The utility StateMachine.mermaid
will generate a mermaid diagram of your state machine. This can be rendered in markdown.
The diagram of Color
above was created using this utility.
StateMachine.mermaid(Green) shouldBeRight """stateDiagram-v2
[*] --> Green
Amber --> Red
Green --> Amber
Red --> Green
""".trimMargin()
The API documentation is published with each release at https://cashapp.github.io/kfsm
See a list of changes in each release in the CHANGELOG.
See lib/src/test/kotlin/app/cash/kfsm/exemplar for a different example of how to use this library.
For details on contributing, see the CONTRIBUTING guide.
Note
kFSM uses Hermit.
Hermit ensures that your team, your contributors, and your CI have the same consistent tooling. Here are the installation instructions.
Activate Hermit either
by enabling the shell hooks (one-time only, recommended) or
manually sourcing the env with . ./bin/activate-hermit
.
Use gradle to run all tests
gradle build