This state machine is inherently unsafe for concurrent use, because the CurrentState and Transitions fields aren't completely protected by a mutex. You already have an unexported mutex that you lock when mutating those fields, but then consumers trying to read those exported fields are not able to lock the same mutex to prevent concurrent read/write operations.
You should not export those fields, and instead make them available via methods where the reads are protected by the mutex. You'll probably also need to make a copy of the map since it's a reference type, OR make the accessor take the key name and fetch the value directly from the map and return it. I learned this when I wrote a similar simple state machine in Go ~7 years ago. :)
I'd also make sure to return `T` from the CurrentState accessor method and not `*T`, just to make it easier for consumers to do comparison operations with the returned result.
Nice job. In my own usage of state machine libraries over the years I have found that for complex use cases, it's definitely helpful to have event-based transition dispatching be part of the library. The great benefit of FSMs is that you can express in data a lot of aspects that you would normally express in code. Not knowing what event needs to happen to transition to one state vs another leaves out a lot of the benefits that you get from designing your code around FSMs.
That being said, I appreciate the simplicity and it's a totally fine choice to leave out event based dispatch for less complex use cases!
One thing that has been mentioned here already: it's super helpful to have your library output a diagram file to visualize the FSM. This is a really great way to keep code and documentation in sync always.
Thank you. The focus was on simplicity and handling less complex scenarios. In my current projects the business logic lends itself to sitting outside the FSM/State Trooper. The logic dictates the transitions at arms length to the FSM. The FSM does not carry on any particular business logic, it just enforces the transition rules from one state to any number of possible states - hence why I've included the metadata aspect to embed info about the reason for a transition.
Re the visualization, I think that would be cool. I might give that a shot.
A state machine should have more than states. It also needs actions which cause transitions. The API should be about writing out which new state an action causes given a base state. Eg you have a modal with a button and it can be clicked or dismissed. In the open state, click and dismiss cause close, and in the closed state, click causes open. Anyway, the whole thing is only valuable once you have actions.
Commenters like you are why people don't share thing. Why make something available for free when someone like you is just going to come along and shit on it?
Interesting. I wrote almost the same code for work a couple of weeks ago.
Not sore, but it looks like the Transition function has a race condition. It calls CanTansition() before acquiring the mutex lock. I think this could lead to illegal state transitions.
I saw the benchmark which seemed crazy (3us per transition and allocations). So looked quickly at the code.
Recommend putting the tracking of previous states in a separate optional debugging type so you don’t have to pay the cost in general. Oh and using time stamps as a key is kinda weird, but even weirder in Go where maps are non-deterministically enumerable.
Otherwise, seems like a good use of generics, given Golangs particular take on it.
A state machine (specifically a FSM) class is something I end up having to reinvent in every new language I've adopted. Such a useful pattern whose need comes up repeatedly. Especially in games/sims or in anything with a GUI. Since I've been making both for decades I have a lot of homegrown FSM classes sitting around. :-p
This program waits until thread(s) is true in another thread until thread(r) is true, everything left of the equals symbol needs to be true before it passes to the next group of facts after the = symbol. Then it waits for "fact(variable)" to be fired, then when all those atoms are true, the state transitions past the pipe symbol and does a send(message) while the other thread does a receive(message) and then the process is repeated in the opposite direction. I've not shown it here, but you can wait for multiple facts to be true too.
Here's a state machine for an (echo) server that is intended to be mapped to epoll or libiouring:
Thanks for everyone's feedback. I've pushed out a few changes:
1- Regular slice instead of timestamp-keyed map. That didn't make sense in retrospect.
2- Better benchmarks.
3- Non-exported current state and transitions. Mutexed getters to avoid concurrency issues.
4- Variadic rule parameters.
5- Better example.
Nice work!! I have always appreciated the clarity of FSMs since first learning about them in college. They are a great way to think about a system's state and given X input, clearly define the next state. This makes them very easy to test as well.
you define Transition over T comparable, but there is no guarantee that comparable implements json.Marshaler, so FSM's MarshalJSON and UnmarshalJSON don't really work
I've written 2 in C++ for different projects at companies State Farm and State of Decay. I'm trying to write one that kind of inverts things and calling it Enemy of the State.
But yes, his name pisses me off because I didnt think of it either.
TheSwordsman|2 years ago
You should not export those fields, and instead make them available via methods where the reads are protected by the mutex. You'll probably also need to make a copy of the map since it's a reference type, OR make the accessor take the key name and fetch the value directly from the map and return it. I learned this when I wrote a similar simple state machine in Go ~7 years ago. :)
I'd also make sure to return `T` from the CurrentState accessor method and not `*T`, just to make it easier for consumers to do comparison operations with the returned result.
Reference on Go memory safety: https://go.dev/ref/mem
hishamk|2 years ago
jupp0r|2 years ago
That being said, I appreciate the simplicity and it's a totally fine choice to leave out event based dispatch for less complex use cases!
One thing that has been mentioned here already: it's super helpful to have your library output a diagram file to visualize the FSM. This is a really great way to keep code and documentation in sync always.
hishamk|2 years ago
Re the visualization, I think that would be cool. I might give that a shot.
earthboundkid|2 years ago
BSEdlMMldESB|2 years ago
> Add valid transitions between states:
> `fsm.AddRule(CustomStateEnumA, CustomStateEnumB)`
> `fsm.AddRule(CustomStateEnumB, CustomStateEnumC)`
packetslave|2 years ago
pokstad|2 years ago
hishamk|2 years ago
rowls66|2 years ago
Not sore, but it looks like the Transition function has a race condition. It calls CanTansition() before acquiring the mutex lock. I think this could lead to illegal state transitions.
hishamk|2 years ago
hmcamp|2 years ago
easeout|2 years ago
icyfox|2 years ago
klabb3|2 years ago
Recommend putting the tracking of previous states in a separate optional debugging type so you don’t have to pay the cost in general. Oh and using time stamps as a key is kinda weird, but even weirder in Go where maps are non-deterministically enumerable.
Otherwise, seems like a good use of generics, given Golangs particular take on it.
hishamk|2 years ago
The benchmark was for six transitions. I've now updated a few things and added new benchmarks.
I also got rid of timestamps for the map keys - it's back to a regular slice. In retrospect, that was a tad bit off, I agree.
syngrog66|2 years ago
samsquire|2 years ago
The runtime is multithreaded and parallel.
The idea is to execute the following state machine:
This program waits until thread(s) is true in another thread until thread(r) is true, everything left of the equals symbol needs to be true before it passes to the next group of facts after the = symbol. Then it waits for "fact(variable)" to be fired, then when all those atoms are true, the state transitions past the pipe symbol and does a send(message) while the other thread does a receive(message) and then the process is repeated in the opposite direction. I've not shown it here, but you can wait for multiple facts to be true too.Here's a state machine for an (echo) server that is intended to be mapped to epoll or libiouring:
The curly brackets means parallel state machines that run independently, like a fork.hishamk|2 years ago
1- Regular slice instead of timestamp-keyed map. That didn't make sense in retrospect. 2- Better benchmarks. 3- Non-exported current state and transitions. Mutexed getters to avoid concurrency issues. 4- Variadic rule parameters. 5- Better example.
victorbjorklund|2 years ago
hishamk|2 years ago
preseinger|2 years ago
MobileVet|2 years ago
preseinger|2 years ago
naikrovek|2 years ago
jnordwick|2 years ago
But yes, his name pisses me off because I didnt think of it either.
hmcamp|2 years ago
bsaul|2 years ago
pjmlp|2 years ago
I really don't get how one can suggest the iota const dance with a straight face as an alternative.
unknown|2 years ago
[deleted]
whywouldudothat|2 years ago
[deleted]
wahnfrieden|2 years ago
[deleted]
Solvency|2 years ago
wahnfrieden: Why the iron oxide name? Besides the fact that it's a pun. Is the code written with it always bound to be rusty and old?
giraffe_lady|2 years ago
[deleted]
brigadier132|2 years ago
oofta-boofta|2 years ago