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) { /*** The functional core of the state machine is suepr trivial to test **/ val events: List = 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 = 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 { fun initialState(): State fun currentState(): State fun handleEvent(event: Event): Command? fun statesHistory(): List fun commandHistory(): List fun eventsHistory(): List // utility functions to model a transition with or without an emitted command fun State.move(): Pair = Pair(this, null) fun State.emit(command: Command?): Pair = Pair(this, command) fun debug() { println(""" Events: ${printList(eventsHistory())} States: ${printList(statesHistory())} Commands: ${printList(commandHistory())} """) } } /*** * Functional Core of our state machine. */ class TurnStyle : StateMachine { 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() override fun statesHistory(): List = history.map { it.first } override fun commandHistory(): List = history.map { it.second } override fun eventsHistory(): List = 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? = 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) = list.joinToString(prefix = "listOf(", postfix = ")") private infix fun 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(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() 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() suspend fun soundTheAlarm(): TurnStyle.Event? { delay(50) say("soundTheAlarm") return MachineRepairDidComplete } private fun say(msg: String) { println(msg) msgs += msg } }