Created
December 22, 2017 19:11
-
-
Save jmfayard/ac6a94df1cc2994ab5b59f510c98133f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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