matthewp / robot

🤖 A functional, immutable Finite State Machine library

Home Page:https://thisrobot.life

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Immutable State Machines

matthewp opened this issue · comments

The goal of this issue is to find an immutable replacement for services.

Many parts of Robot are already immutable, machines and their state for example, but services are the one glaring exception. This causes a number of problems internally, mostly around services carrying around stale state in the middle of transitions.

I would like to research this topic a bit more and look to the wider software world for inspiration. It's important to first consider why do we have services in the first place? There could be a send() function that takes a machine and returns a new machine (this is kind of how it already works internally).

  • Services hold on to an onChange function that gets called when send() is called. How can we replace this?
  • Services hold the context object.
  • Services hold on to the child which is a child service. I think we can move this to the machine where child is a child machine but I'm not totally sure about this, this is sort of stateful.

One way to think of the onChange problem is that onChange is like an event, so services are kind of event emitters. Event emitters are inherently mutable. So we need something different from onChange in order to solve this.

Are you still looking to change service to something immutable? Or is the plan to create another immutable interface side by side?

Would replace services but this is very far off from happening (if it were to ever happen). It's probably not possible without getting rid of invoke.

Really happy to see this being discussed here. I've worked with xstate-react a bit in the past, and the inherent statefulness in its services introduces a lot of impedance mismatch with react's reactive model. xstate services specifically are not able to robustly react to prop changes because of their inherent statefulness (statelyai/xstate#396 (comment)), which greatly limits their expressive power.

Drawing from my limited experience of working with fsm libraries in functional languages (https://github.com/ztellman/automat, https://github.com/metosin/tilakone), I think a more powerful state machine abstraction could be modeled as a pure reducer function that takes some current state (that could include the machine configuration itself) and an event, and returns the next state (and machine configuration). And in fact, lately I've mostly been using the useReducer function in react with ad-hoc state-machine-like internal data structures as state over static fsm based solutions like xstate-react.

The approach is certainly not without tradeoffs though, the gains in expressiveness that we get through the ability to reconfigure our machine dynamically means that when we do build dynamic machines like this, we can't as easily analyze our machine and visualize all possible states and state transitions through traditional state machine tooling that tends to assume a static set of states and transitions. I'm personally ok with this tradeoff because 1) in the short term we can always fall back to defining a machine with a static set of states and transitions in cases where we find this kind of analysis/visualization to be useful, since that's just a special case of the more powerful dynamic machine abstraction (and we can create abstractions to better support this special case as well), and 2) in the longer term I'm sure tooling will emerge to help us reason about and formalize these dynamically evolving state machines as well.

As for what this could mean for robot in more concrete terms, I personally really loved reading this:

There could be a send() function that takes a machine and returns a new machine (this is kind of how it already works internally).

Since to me this sounds like services in robot currently can almost already be used as if it was a pure reducer function. I think what's missing is an alternative implementation of interpret that doubles down on the state machine as a reducer concept, and simply serves directly as a pure reducer function that takes current state (including the machine) and an event, and returns the updated state (and machine).

Then a "service" would simply be an optional stateful runtime construct that keeps track of the current state and accepts a stream of events to reduce onto the current state (we end up getting history and time travel for free with this approach as well), but this "service" would only be used in cases where we want to use the state machine in isolation. When we want to use the machine in inherently stateful contexts like react, which has its own state management mechanisms, we can forgo our own "service" implementation altogether and simply use existing state management constructs like useReducer and delegate all the stateful parts of the machine execution to the react runtime, which should result in a much simpler and significantly more robust integration (the current react-robot integration seems to suffer from the same shortcoming as xstate-react, in that it also creates an inherently stateful "service" that operates independently of react state, and has to be carefully kept in sync).

commented

any news on this? I'd love to be able to do something like xstate's machine.transition(initial, {type}).value, and it could be nice if the API was even simpler than that