You already know, Erlang is my favorite language... You should also probably know Erlang uses finite state machines as one of its core concept? In fact, it's probably why one can't come back from Erlang, when you know how to use them, everything becomes easier. This kind of pattern is not only available in Erlang, and few implementations exist on Dart. Here the interesting ones found on pub.dev and github:
| project | version | documentation | source code |
|---|---|---|---|
state_machine |
3.0.3 |
documentation | source code |
statemachine |
3.4.0 |
documentation | source code |
automata |
0.2.0 |
documentation | - |
dart_fsm |
1.3.1 |
documentation | source code |
immutable_fsm |
1.1.0 |
documentation | source code |
The most used of those project is state_machine with more than 22k download, then, this will be the first one to be tested in this article.
Bootstrapping
$ dart create fsm
Creating fsm using template console...
.gitignore
analysis_options.yaml
CHANGELOG.md
pubspec.yaml
README.md
bin/fsm.dart
lib/fsm.dart
test/fsm_test.dart
Running pub get... 1.1s
Resolving dependencies...
Downloading packages...
Changed 48 dependencies!
Created project fsm in fsm! In order to get started, run the following commands:
cd fsm
dart run
$ cd fsm
$ dart pub add state_machine
Resolving dependencies...
Downloading packages...
+ clock 1.1.2
+ intl 0.20.3
+ js 0.6.7 (0.7.2 available)
+ state_machine 3.0.3
+ w_common 4.0.0
Changed 5 dependencies!
1 package has newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.
The only required header to add in all your code requiring this module:
import 'package:state_machine/state_machine.dart';
Usage
The first step is to create a new StateMachine object. Only one argument is required for the constructor, the state machine name.
When a state machine changes its state, the onStateChange parameter is receiving the event, and then, react on it.
// create a new state machine
StateMachine fsm = StateMachine('fsm');
// react on state change with a closure
final onStateChange = (StateChange change) {
print('onStateChange: $change');
};
fsm.onStateChange.listen(onStateChange);
The second step is to create a new State objects representing respectively all accepted state by the state machine. To do that, the newState() method must be called. The only argument to pass is the name of the state.
During a transition, the state machine can enter into a new state or leave an old state. Those two kind of events can be followed by configuring respectively the onEnter and onLeave parameters.
// enter state closure
final onEnter = (StateChange change) {
print('onEnter: $change');
};
// leave state closure
final onLeave = (StateChange change) {
print('onLeave: $change');
};
// create a state called init
State init = fsm.newState('init');
init.onEnter.listen(onEnter);
init.onLeave.listen(onLeave);
// create a state called count
State count = fsm.newState('count');
count.onEnter.listen(onEnter);
count.onLeave.listen(onLeave);
// create a state called stop
State stop = fsm.newState('stop');
stop.onEnter.listen(onEnter);
stop.onLeave.listen(onLeave);
The transition between states can then be created. They are represented by StateTransition objects. This is the most "complex" method there, because the method newStateTransition requires 3 arguments. One is the name of the transition as a string, the second is the list of allowed states before the transition, and the last one is the next state after the transition.
StateTransition counting = fsm.newStateTransition('counting', [init,count], count);
StateTransition stopping = fsm.newStateTransition('stopping', [count], stop);
Then, the state machine can be started by calling the start() method. The only argument used here is a reference to the starting state.
fsm.start(init);
The transitions can now be called using the previous objects created. It is also possible to pass an argument to them as a payload.
// calling the counting transition
counting(1);
counting(2);
counting(3);
counting(4);
// calling the stopping transition
stopping();
When the state machine is terminated, one should use the dispose() method to clean up the memory and avoid memory leaks.
fsm.dispose();
The code looks okay. Let execute it to see what will happen.
$ dart run
...
onStateChange: StateChange: (none) --> init
onEnter: StateChange: (none) --> init
onStateChange: StateChange: init --> count
payload: 1
onLeave: StateChange: init --> count
payload: 1
onStateChange: StateChange: count --> count
payload: 2
onLeave: StateChange: count --> count
payload: 2
onStateChange: StateChange: count --> count
payload: 3
onLeave: StateChange: count --> count
payload: 3
onStateChange: StateChange: count --> count
payload: 4
onLeave: StateChange: count --> count
payload: 4
onStateChange: StateChange: count --> stop
onLeave: StateChange: count --> stop
onEnter: StateChange: count --> stop
onDispose: null
As you can see, the different steps are displayed on the screen, and we can see each state transitions. If an illegal transition happens in a specific state, the state machine throw an exception.
Conclusion
state_machine just works, but the way the state machine, the transitions and the events triggering the transitions are created annoys me a bit, perhaps because it's the OOP way to do it.
Indeed, to me, a great implementation of a state machine is from Erlang, with gen_statem, a state machine should embed its "data" or "state" and those elements should be modified during a transition.
Anyway, if you want to know more about the state_machine package, here few links:
state_machinepackage on pub.dev;state_machineofficial API documentation on pub.dev;state_machinesource code on Github;state_machineexample source code on Github;state_machinetest suite source code on Github.
Have fun and happy hacking!
Cover Image by Mike Hindle on Unsplash
Top comments (4)
The breakdown of using the
state_machinemodule in Dart is quite enlightening, especially how you've highlighted the simplicity of bootstrapping a finite state machine. It's interesting to see how theonStateChangelistener can effectively manage state transitions, which is crucial for building responsive applications. One potential improvement could be to explore integrating this with Dart’s built-in streams for more complex state handling scenarios. If you’re considering enhancements or new features for this state machine implementation, I’d be open to collaborating on that front!Actually, I'm not comfortable with the way it's done in Dart. I think most of the state machine should be specified on a lower level, and should using more function/closure to deal with the state itself. I would like to recreate something similar than what we are doing in Erlang, here and example:
This is a really simplified version of a
gen_statem, but as you can see, the state transition is done when some events are happening, and the data are directly linked to the state itself (and then the transition). Furthermore, thanks to Erlang actor model, side-effects are isolated from the other processes.I already thought a bit about that for Dart, but I'm still struggling with a simple interface and a simple way to define the state itself. Again, in Erlang, it's simple. An event is a message, this concept is not directly present in Dart, exception if one is using an
Isolatewith some kind of interfaces to simulate a message.Anyway, it will be the subject from another post. ;)
state_machine has an unknown license. statemachine has fewer downloads, but it is written by the prolific author of petitparser, which also became the core for the XML parser. Sharp code all around.
It will be the subject of another article. In fact,
statemachineandstate_machinewere both on my list, but I needed to pick one, I used the download criteria.I currently try to find something close to Erlang/Elixir development experience... I did not find it yet. I don't think it's really possible to have the same, but perhaps you have few good projects name on your side? :)
Thanks again for all these comments! :)