Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
package statemachine
import debug
import fail
import io.reactivex.Observable
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.produce
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
import statemachine.TurnStyle.Event.*
import statemachine.TurnStyle.Command.*
import statemachine.TurnStyle.State.*
import java.lang.Thread.sleep
/***
* Context: I highly recommend andymatuschak's gist
*
* A composable pattern for pure state machines with effects
* https://gist.github.com/andymatuschak/d5f0a8730ad601bcccae97e8398e25b2
*
* It's written in swift but nicely maps to Kotlin as demonstrated here
*
* See the schema of the TurnStyle here
*
* ![TurnStyle](https://camo.githubusercontent.com/a74ea94a7eab348f991fb22d6f70a92c5bef3740/68747470733a2f2f616e64796d617475736368616b2e6f72672f7374617465732f666967757265332e706e67)
***/
fun main(args: Array<String>) {
/*** The functional core of the state machine is suepr trivial to test **/
val events: List<TurnStyle.Event> = listOf(
InsertCoin(20), InsertCoin(20), InsertCoin(10),
AdmitPerson,
InsertCoin(1),
MachineDidFail,
MachineRepairDidComplete)
val expectedStates = listOf(
Locked(0), Locked(20), Locked(40), Unlocked,
Locked(0), Locked(1), Broken(Locked(1)), Locked(0)
)
val expectedCommands: List<TurnStyle.Command?> = listOf(null, null, null, OpenDoors, CloseDoors, null, null, null)
val stateMachine = TurnStyle()
events.forEach { e -> stateMachine.handleEvent(e) }
stateMachine.debug()
stateMachine.statesHistory() shouldBe expectedStates
stateMachine.commandHistory() shouldBe expectedCommands
/** The imperative shell takes care of the side Effects **/
runBlocking {
val controller = runStateMachineWithSideEffects()
controller.customerDidInsertCoin(10)
delay(100)
controller.customerDidInsertCoin(50)
delay(100)
// controller.shitHappens()
delay(2000)
controller.stateMachine.debug()
controller.doorHardwareController.msgs shouldBe listOf("sendControlSignalToOpenDoors", "sendControlSignalToCloseDoors")
}
}
/** Generic State Machine **/
interface StateType
interface StateEvent
interface StateCommand
interface StateMachine<State : StateType, Event : StateEvent, Command : StateCommand> {
fun initialState(): State
fun currentState(): State
fun handleEvent(event: Event): Command?
fun statesHistory(): List<State>
fun commandHistory(): List<Command?>
fun eventsHistory(): List<Event>
// utility functions to model a transition with or without an emitted command
fun State.move(): Pair<State, Command?> = Pair(this, null)
fun State.emit(command: Command?): Pair<State, Command?> = Pair(this, command)
fun debug() {
println("""
Events: ${printList(eventsHistory())}
States: ${printList(statesHistory())}
Commands: ${printList(commandHistory())}
""")
}
}
/***
* Functional Core of our state machine.
*/
class TurnStyle : StateMachine<TurnStyle.State, TurnStyle.Event, TurnStyle.Command> {
override fun initialState(): TurnStyle.State = State.Locked(credit = 0)
override fun currentState(): State = history.last().first
private val history = mutableListOf(initialState() to doNothing)
private val events = mutableListOf<Event>()
override fun statesHistory(): List<State> = history.map { it.first }
override fun commandHistory(): List<Command?> = history.map { it.second }
override fun eventsHistory(): List<Event> = events.toList()
sealed class State(val msg: String? = null) : StateType {
data class Locked(val credit: Int) : State()
object Unlocked : State("Unlocked")
data class Broken(val oldState: State) : State()
override fun toString(): String =
msg ?: super.toString()
}
sealed class Event(val msg: String? = null) : StateEvent {
data class InsertCoin(val value: Int) : Event()
object AdmitPerson : Event("AdmitPerson")
object MachineDidFail : Event("MachineDidFail")
object MachineRepairDidComplete : Event("MachineRepairDidComplete")
override fun toString(): String =
msg ?: super.toString()
}
enum class Command : StateCommand {
SoundAlarm, CloseDoors, OpenDoors
}
override fun handleEvent(event: Event): Command? {
events += event
val currentState = currentState()
val nextMove: Pair<State, Command?>? = when (currentState) {
is Locked -> when (event) {
is Event.InsertCoin -> {
val newCredit = currentState.credit + event.value
if (newCredit >= FARE_PRICE)
Unlocked.emit(OpenDoors)
else
Locked(newCredit).move()
}
AdmitPerson -> currentState.emit(SoundAlarm)
MachineDidFail -> Broken(oldState = currentState).move()
MachineRepairDidComplete -> null
}
Unlocked -> when (event) {
AdmitPerson -> Locked(credit = 0).emit(CloseDoors)
else -> null
}
is Broken -> when (event) {
MachineRepairDidComplete -> Locked(credit = 0).move()
else -> null
}
}
if (nextMove == null) {
fail("Unexpected event $event from state $currentState")
} else {
history.add(nextMove)
return nextMove.second
}
}
companion object {
private val doNothing: Command? = null
const val FARE_PRICE = 50
}
}
private fun printList(list: List<Any?>) = list.joinToString(prefix = "listOf(", postfix = ")")
private infix fun <T> T?.shouldBe(expected: Any?) {
if (this != expected) error("ShouldBe Failed!\nExpected: $expected\nGot: $this")
}
/***
Now, an imperative shell that hides the enums and delegates to actuators.
Note that it has no domain knowledge: it just connects object interfaces.
***/
suspend fun runStateMachineWithSideEffects(): TurnStyleController {
val controller = TurnStyleController(DoorHardwareController(), SpeakerController(), TurnStyle())
launch { controller.consumeEvents() }
return controller
}
class TurnStyleController(
val doorHardwareController: DoorHardwareController,
val speakerController: SpeakerController,
val stateMachine: TurnStyle
) {
private val events = Channel<TurnStyle.Event>(5)
suspend fun consumeEvents() {
for (event in events) {
if (event == MachineDidFail) {
askSomeoneToRepair()
}
val command = stateMachine.handleEvent(event)
val nextEvent = handleCommand(command)
if (nextEvent != null) events.send(nextEvent)
}
stateMachine.debug()
}
suspend fun shitHappens() {
events.send(MachineDidFail)
}
suspend fun askSomeoneToRepair() {
delay(700)
events.send(MachineRepairDidComplete)
}
suspend fun customerDidInsertCoin(value: Int) {
events.send(InsertCoin(value))
}
suspend fun handleCommand(command: TurnStyle.Command?): TurnStyle.Event? {
val nextEvent: TurnStyle.Event? = when (command) {
OpenDoors -> doorHardwareController.sendControlSignalToOpenDoors()
SoundAlarm -> speakerController.soundTheAlarm()
CloseDoors -> doorHardwareController.sendControlSignalToCloseDoors()
null -> null
}
return nextEvent
}
}
class DoorHardwareController() {
val msgs = mutableListOf<String>()
suspend fun sendControlSignalToOpenDoors(): TurnStyle.Event? {
delay(500)
say("sendControlSignalToOpenDoors")
return AdmitPerson
}
suspend fun sendControlSignalToCloseDoors(): TurnStyle.Event? {
delay(100)
say("sendControlSignalToCloseDoors")
return null
}
private fun say(msg: String) {
msgs += msg
println(msg)
}
}
class SpeakerController {
val msgs = mutableListOf<String>()
suspend fun soundTheAlarm(): TurnStyle.Event? {
delay(50)
say("soundTheAlarm")
return MachineRepairDidComplete
}
private fun say(msg: String) {
println(msg)
msgs += msg
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment