I have made an attempt at implementing a StateMachine in Scala, however I have encountered an issue with the type system that has me rather baffled. In the code below, I need to have the guard function accept an argument of an expected sub-class of StateMachine. Unfortunately, as the type parameter for FunctionN arguments are contravariant, I am not sure how to pull this off.
class Transition[S, +M <: StateMachine[S]](start: S, end :S, var guard: Option[M => Boolean]) {
// COMPILER ERROR ABOVE LINE : ^^ covariant type M occurs in contravariant position in type => Option[M => Boolean] of method guard ^^
val startState = start
val endState = end
def willFollow(stateMachine: M, withGuard : Boolean) =
// COMPILER ERROR ABOVE : ^^ covariant type M occurs in contravariant position in type M of value stateMachine ^^
if (!withGuard && guard == None) true;
else (withGuard && guard.get(stateMachine))
}
class EpsilonTransition[S, M <: StateMachine[S]](start: S,end :S) extends Transition[S, M](start, end, None)
class StateMachine[S](transitions: Set[Transition[S, StateMachine[S]]], initialStates: Set[S]) {
private val stateDrains = transitions.groupBy(_.startState);
private var activeStates = initialStates
def act() = {
var entryStates = Set[S]()
var exitStates = Set[S]()
stateDrains.foreach {drain =>
val (exitState, transitionsOut) = drain
// Follow non-epsilon transitions
transitionsOut.filter(_.willFollow(this, true)).foreach {transition =>
exitStates += transition.startState
entryStates += transition.endState
}
}
// For all exit states we map the state to a set of transitions, and all sets are "flattened" into one big set of transitions
// which will then filter by those which do not have a guard (epsilon transitions). The resulting filtered list of transitions
// all contain endStates that we will map to. All of those end states are appended to the current set of entry states.
entryStates = entryStates ++ (exitStates.flatMap(stateDrains(_)).filter(_.willFollow(this, false)).map(_.endState))
// Exclude only exit states which we have not re-entered
// and then include newly entered states
activeStates = ((activeStates -- (exitStates -- entryStates)) ++ entryStates)
}
override def toString = activeStates.toString
}
object HvacState extends Enumeration {
type HvacState = Value
val aircon, heater, fan = Value
}
import HvacState._
object HvacTransitions {
val autoFan = new EpsilonTransition[HvacState, HVac](aircon, fan)
val turnOffAc = new Transition[HvacState, HVac](aircon, fan, Some(_.temperature 75))
val HeaterToFan = new Transition[HvacState,HVac](heater, fan, Some(_.temperature > 50))
}
import HvacTransitions._
class HVac extends StateMachine[HvacState](Set(autoFan, turnOffAc, AcToHeater, HeaterToAc, HeaterToFan), Set(heater)) {
var temperature = 40
}