Please note: Despite me using Swift-lang in the examples, I encourage you to try and help me even if you know RxJava and rxjs or any other Rx implementation.
Question: I have a state machine that is written imperatively. I would like to rewrite reactively using Rx but I am struggling with coming up with the ways to do it proper.
Current imperative system looks like this:
var state: State
var metaState: MetaState
func updateMetaStateIfNeeded() {
if Bool.random() {
metaState = MetaState()
}
}
func tick() -> State {
updateMetaStateIfNeeded()
let newState = State(currentState: state, metaState: metaState)
self.state = newState
return newState
}
What I came up with so far:
let tick = PublishSubject<Void>()
let metaState = BehaviorSubject<MetaState>()
let state = BehaviorSubject<State>()
let tickAfterMetaStateUpdate = PublishSubject<Void>()
tick -> withLatestFrom metaState -> map to new metaState if update needed or the old one -> put result into metaState AND tickWithUpdatedMetaState
tickAfterMetaStateUpdate -> withLatestFrom state AND metaState -> map State(currentState: state, metaState: metaState) -> put result into state
(The users of this API subscribe to state subject, which will be updated every tick)
I am dissatisfied with this result I came up with. I wonder if it's possible to rewrite this using scan or reduce so that there is no usage of Subjects and it's a plain Observable.
UPDATE after @Daniel T.'s answer:
Wow, this was exactly the proper solution! I have prepared the analytics:
- There are 2 state machines, one for
metaStateupdate, one forstateupdate - Each of them has it's own start states which we don't need to specify
- The input alphabet for
metaStateisVoid, a mere presence of a tick. Forstate, it's themetaStateoutput of the first state machine
So with this understanding, I have rewriten the system using 2 scans:
let state = tick
.scan(MetaState()) { currentMetaState, void in
if Bool.random() {
return MetaState()
}
return currentMetaState
}
.scan(State()) { currentState, currentMetaState in
State(currentState: state, metaState: metaState)
}
WINRAR! Mind_blown.jpg! Actually, now with hind-sight it kinda seems obvious... :) Thank you, thank you, thank you!
Hmm... Yes,
scanis the go to operator for implementing a state machine. To do it, you need a finite set of states (usually implemented as a struct, but it could be an enum,) a start state (usually implemented using a default constructor on the State struct,) an input alphabet (usually implemented as an enum, often called some variant of Event, Command or Action,) and a transition function (the closure passed to thescan.)So the real question here is what are the commands/actions that change the state? Most of the time, the actions are produced by user actions, but your question doesn't really give an indication of what actions are, what the input alphabet is...
Sadly, there isn't enough information in the question to give more details than that.
-- UPDATE --
Sorry, but your update is not correct. The closure passed to
scanshould be pure and the first one is most assuredly not. Even if theMetaState.init()is pure, theBool.random()isn't.The only time you should pass an impure closure into an Observable is when you are creating a new Observable, consuming the final result, or within a
do.A simple fix to what you have looks like this:
It's a minor change for sure, but it more clearly shows the intent of the code and makes clear what can, and can't be unit tested.