3 ms·
What do you mean by "a certain event-sourcing something idea?" I really do mean "pure." That doesn't mean that Redux doesn't provide other facilities on top, s
by dwohnitmok 6y ago
What do you mean by "a certain event-sourcing something idea?"
I really do mean "pure." That doesn't mean that Redux doesn't provide other facilities on top, such as React interop, error-handling, logging, etc. But if you don't need any of that and you use the core of Redux, it really is just a state machine representation.
Let's write out the classic finite state machine representation of a locked/unlocked item.
States: Locked, Unlocked
State transition table:
Current State | Transition | Next State
---------------------------------------
Locked | Lock | Locked
Unlocked | Lock | Locked
Locked | Unlock | Unlocked
Unlocked | Unlock | Unlocked
Initial State: Unlocked
That's a textbook definition of an FSM.
This is the equivalent Redux code.
import { createStore } from 'redux';
const initialState = 'Unlocked';
const reducer = (currentState, transition) => {
if (currentState === 'Locked' && transition === 'Lock') {
return 'Locked';
} else if (currentState === 'Locked' && transition === 'Unlock') {
return 'Unlocked';
} else if (currentState === 'Unlocked' && transition === 'Lock') {
return 'Locked';
} else if (currentState === 'Unlocked' && transition === 'Unlock') {
return 'Unlocked';
}
};
const store = createStore(reducer, initialState);
// We're done! Now we can play with our state machine
console.log(store.getState()); // 'Unlocked'
store.dispatch('Lock');
console.log(store.getState()); // 'Locked'
store.dispatch('Lock');
console.log(store.getState()); // 'Locked'
store.dispatch('Unlock');
console.log(store.getState()); // 'Unlocked'
That's it. That's Redux. This is exactly a 1-to-1 translation of the FSM. I don't see how it could be any more direct (at least in JS). Everything else in Redux is optional.
- darksaints 6y agoIf you think that's the simplest you could get, more power to you. I personally think your example is convoluted for such a simple example, but that's just me. However, I should point out something that is not actually trivial about your code, by proposing a refactor: your FSM adequately models a car lock button interface, but not the lock itself. If we are modeling the lock, you have two invalid transitions...because it is impossible to lock an already-locked lock, and it is impossible to unlock an already-unlocked lock. Classes actually shine quite well here. class On { constructor(){ console.log('turned on') } turnOff(){ console.log('turning off...') return new Off(); } } class Off { constructor(){ console.log('turned off') } turnOn(){ console.log('turning on...') return new On(); } } const toggle = new On(); toggle.turnOff().turnOn().turnOff().turnOff() // ^ cool, ^ cool, ^ cool. ^ oh shit, typescript really doesn't want to let me do this. Classes can model your original scenario quite easily (just add a `turnOn()` method to `On` and `turnOff()` to `Off`, but redux can't model my scenario (at least not without more boilerplate and lots of guards). More importantly, the available actions are localized...I don't have to create a monolithic state transition table logic...I only have to concern myself with the possible transitions for any given state. This makes it trivial to add new states or new actions, because I will never have to worry about the combinatorial explosion that can happen in a global transition table.
- dwohnitmok 6y ago> I personally think your example is convoluted for such a simple example, but that's just me. It is indeed all a matter of taste in the end, but I mean it's the exact transcription of a textbook FSM. I don't think your example would get that much simpler with a stable identifier, which brings me to: > I don't have to create a monolithic state transition table logic It doesn't have to be monolithic. I just created a monolithic function because it's easier for this small example. It could just as easily dispatch on state (which is what you're doing here), or on transition (which is impossible with your hierarchy), or mix and match them. I can write those examples if you're curious. Your class example is a good example of a state machine trace (e.g. one that you might often use to create an ephemeral config object), but it's not an example of an actual persistent state machine. As soon as you use a stable identifier the type safety goes out the window. let stateMachine = new On(); // Wait a bit for the user to do something stateMachine = statemachine.turnOff(); // Wait again for the user to do something stateMachine = stateMachine.turnOn(); Depending on how you annotate the initial let, one of those two lines will cause TS to complain (or you have to do a manual cast somewhere which circumvents the type safety). Indeed if you just care about a state machine trace the same type safety holds in the explicit state transition table case if you just use TS's literal types in the reducer's type annotations. You just explicitly call the reducers you need however many times and then persist the state machine at the very end. That is your type safety doesn't come from the representation as an object, but rather that there is no stable state machine between invocations, but only an ephemeral trace of one whose intermediate states are immediately destroyed. Traces are valuable! But you could generate the exact same trace with an explicit state transition function and you would get the exact same type safety that way. I can write it out for you if you're curious. > I will never have to worry about the combinatorial explosion that can happen in a global transition table. Again you don't have to worry about a combinatorial explosion in a global transition table either if you dispatch on actions or states. The fundamental difference between what you've outlined here and the explicit state transition table is that in your approach states are first-class and transitions are not (but rather methods attached to states), whereas the explicit state transition function treats both of them as first-class entities. For places where you really only need an ephemeral state machine trace, rather than a state machine that persists between calls, you don't need first-class transitions because the transitions are all ephemeral and cannot be dynamically called at runtime. Where you do have users able to dynamically call transitions at runtime, you end up needing to represent those transitions somehow, and you end with something approaching Redux (indeed I think an interesting exercise would be to implement On and Off where the user either presses "a" or "b" at the keyboard to turn on and off with your classes. I suspect you end up with just the Redux approach all over again). EDIT: I want to emphasize I don't think Redux is free of faults. There are many things I really dislike about it and the React ecosystem it integrates into. But I don't think any of that can be chalked up to a poor representation of a state machine.
- nsonha 6y agoLook every of your comment is really tiring to read, so fucking long with little substance like redux code. Everyone already knows what redux is, it's really garbage and leaky. For one there is a dispatch function, where is this in the classic "pure" state machine? Why can't state transition be just a function call? Secondly there is no type that you can infer from this, you have to "write typescript" by adding type annotations like an idiot. Or else you could dispatch anything because again there need to be a dispatch function because reason. The other commenter gave example of implementation as type safe immutable builder. You can even simplify their implementation further by using plain functions instead of classes.