andyb129
12/22/2017 - 7:11 PM

See https://gist.github.com/andymatuschak/d5f0a8730ad601bcccae97e8398e25b2

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
    }

}