rxjsrx-javareactive-programmingrx-swiftfrp

How do I properly organize this Rx-based reactive state machine?


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:

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!


Solution

  • Hmm... Yes, scan is 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 the scan.)

    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 scan should be pure and the first one is most assuredly not. Even if the MetaState.init() is pure, the Bool.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:

    let state = tick
        .flatMap { // this closure is an Observable factory. You can do impure things here.
            Observable.just(Bool.random() ? MetaState() : nil)
        }
        .scan(State()) { currentState, metaState in // this closure should be pure.
            if let metaState = metaState {
                return State(currentState: currentState, metaState: metaState)
            }
            else {
                return currentState
            }
        }
    

    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.