Skip to content
infosmid-orgPublic

Latest commit

 

History

245 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kotlin Finite-state machine

Build Status Maven Central License

This work is licensed under Apache License 2.0

kfsm is a small Kotlin library for finite state machines. It has a Kotlin DSL.

Getting Started

In kfsm, an event can cause a transition from one state to another state. A transition can run an action. It can also run an entry action and an exit action.

Features

  • Event-driven state machine.

  • External transitions and internal transitions.

  • Entry actions and exit actions for each state.

  • Default actions for each state.

  • Default entry actions and default exit actions.

  • A query for the allowed events.

  • Multiple state maps with push transitions and pop transitions.

  • Automatic transitions.

  • Externalized state.

  • Typed event arguments and return values.

Authors

Star History

Star History link="https://star-history.com/#infosmid-org/kfsm&Date"

Todo

  • ✓ Multiple state maps

  • ✓ Push transitions and pop transitions

  • ✓ Automatic transitions

  • ✓ Externalized state

  • ✓ Typed event arguments

  • ✓ Typed event return values

  • ✓ Simple visualization

  • ✓ Detailed visualization

  • ✓ Gradle plugin for visualization

  • ✓ Timeouts

  • ✓ Coroutines

  • ❏ A different context type for nested state maps

Quick Tutorial

This is the classic turnstile state machine from SMC.

Simple turnstile example

Assume that you must manage the state of a simple lock. The lock() function must run only when the lock is not locked. The unlock() function must run only when the lock is locked.

Use the DSL to declare a state machine definition that matches the diagram:

State Diagram

Turnstile state diagram

State Table

Start State Event End State Action

LOCKED

PASS

LOCKED

alarm

LOCKED

COIN

UNLOCKED

unlock

UNLOCKED

PASS

LOCKED

lock

UNLOCKED

COIN

UNLOCKED

returnCoin

Context class

class Turnstile(var locked: Boolean = true) {
    fun unlock() {
        assert(locked) { "Cannot unlock when not locked" }
        println("Unlock")
        locked = false
    }

    fun lock() {
        assert(!locked) { "Cannot lock when locked" }
        println("Lock")
        locked = true
    }

    fun alarm() {
        println("Alarm")
    }

    fun returnCoin() {
        println("Return coin")
    }
    override fun toString(): String {
        return "Turnstile(locked=$locked)"
    }
}

Enums for States and Events

Declare two enums. One enum has the states. The other enum has the events.

enum class TurnstileStates {
    LOCKED,
    UNLOCKED
}

enum class TurnstileEvents {
    COIN,
    PASS
}

Packaged definition and execution

class TurnstileFSM(turnstile: Turnstile) {
    private val fsm = definition.create(turnstile)

    fun coin() = fsm.sendEvent(TurnstileEvents.COIN)
    fun pass() = fsm.sendEvent(TurnstileEvents.PASS)
    companion object {
        private val definition = stateMachine(
            TurnstileStates.values().toSet(),
            TurnstileEvents::class,
            Turnstile::class
        ) {
            initialState {
            if (locked)
                TurnstileStates.LOCKED
            else
                TurnstileStates.UNLOCKED
            }
            default {
                onEntry { startState, targetState, _ ->
                    println("entering:$startState -> $targetState for $this")
                }
                // default transition will invoke alarm
                action { state, event, _ ->
                    println("Default action for state($state) -> event($event) for $this")
                    alarm()
                }
                onExit { startState, targetState, _ ->
                    println("exiting:$startState -> $targetState for $this")
                }
            }
            // when current state is LOCKED
            whenState(TurnstileStates.LOCKED) {
                // external transition on COIN to UNLOCKED state
                onEvent(TurnstileEvents.COIN to TurnstileStates.UNLOCKED) {
                    unlock()
                }
            }
            // when current state is UNLOCKED
            whenState(TurnstileStates.UNLOCKED) {
                // internal transition on COIN
                onEvent(TurnstileEvents.COIN) {
                    returnCoin()
                }
                // external transition on PASS to LOCKED state
                onEvent(TurnstileEvents.PASS to TurnstileStates.LOCKED) {
                    lock()
                }
            }
        }.build()
    }
}

This definition says:

  • When the state is LOCKED and the event is COIN, change to UNLOCKED and run the lambda { unlock() }. The lambda runs as a member of the context.

  • When the state is LOCKED and the event is PASS, run the default action alarm(). The state stays LOCKED.

Usage

Create an instance of the state machine. Give it the context that the actions operate on:

val turnstile = Turnstile()
val fsm = TurnstileFSM(turnstile)

The context is independent of the state machine.

When you send an event, the state machine can run actions:

// The state is LOCKED
fsm.coin()
// Expect the unlock() action. The new state is UNLOCKED
fsm.pass()
// Expect the lock() action. The new state is LOCKED
fsm.pass()
// Expect the alarm() action. The state stays LOCKED
fsm.coin()
// Expect the unlock() action. The new state is UNLOCKED
fsm.coin()
// Expect the returnCoin() action. The state stays UNLOCKED

You can create an instance of the state machine when you need it, if the context has values that represent the state. The context must keep its own state correctly.

The state machine can derive the state from the values of the properties of the context.

The Documentation has more detail about how to define state machines.

The documentation has examples for:

Repository

Releases are on Maven Central. Snapshots are in the GitHub Packages Maven repository. Contact us for a personal access token (PAT) to get snapshot releases.

Dependencies

Kotlin Multiplatform (build.gradle.kts)

repositories {
    mavenCentral()
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("org.infosmid:kfsm:2.0.0")
        }
    }
}

Kotlin Multiplatform (build.gradle - Groovy DSL)

repositories {
    mavenCentral()
}

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation 'org.infosmid:kfsm:2.0.0'
            }
        }
    }
}

Single Platform (JVM / Android)

Kotlin DSL (build.gradle.kts)
repositories {
    mavenCentral()
}

dependencies {
    implementation("org.infosmid:kfsm:2.0.0")
}
Groovy DSL (build.gradle)
repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.infosmid:kfsm:2.0.0'
}

Gradle version catalog (gradle/libs.versions.toml)

Declare the library in the version catalog:

[versions]
kfsm = "2.0.0"

[libraries]
infosmid-kfsm = { module = "org.infosmid:kfsm", version.ref = "kfsm" }

Then use the catalog accessor in the build script.

For Kotlin Multiplatform

Kotlin Multiplatform

repositories {
    mavenCentral()
}

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation(libs.infosmid.kfsm)
            }
        }
    }
}

For a single platform (JVM / Android)

Single Platform

repositories {
    mavenCentral()
}

dependencies {
    implementation(libs.infosmid.kfsm)
}

Visualisation

For more information about the visualization options, see kfsm-viz.

Plantuml Examples

Turnstile FSM

turnstile

Paying Turnstile FSM

paying turnstile

Secure Turnstile FSM

secure turnstile

Packet Reader FSM

packet reader

Questions

  • Should entry actions and exit actions receive the state or the event as arguments?

  • Should default actions receive the state or the event as arguments?

  • Is there a better way to define states and events with sealed classes?

  • Does the implementation lack any features?

Releases

Packages

Used by

Contributors

Languages