Hello,
I'd like to bring to the public my exercises with event driven states machines in nim-lang. General architecture of an application looks like this:
SM == "State Machine"
MD == "Message Dispatcher"
MQ == "Message Queue"
EC == "Event Capture"
OS == "Operating System"
SM SM SM ...
| ^^
| ||
| MD
\ ^
-> / MQ /
/
|
<--- EC (epoll, kqueue)
^
| (i/o, timers, signals, fs events etc.)
|
OS
Source is here (state diagrams included, see jpeg file)
This example demonstrates:
I have been using this design methodology for almost 10 years for constructing various network programs (in C) and my observation is that it is quite good way to obtain fine grain concurrency within single thread/process and so can be used as alternative to, say, async/await or coroutines/greenlets/fibers
Since I'm newbie (to put it lightly) in nim-lang, any critical comments are welcome.
if you want feedback on your code you're probably better off sharing it as a GitHub repository
Sorry for long delay, now it's on github. See also implementations in C, D and Zig .
But what about Rust, Odin, Crystal, Val, C++, Carbon, Swift, Go, Julia?
It was a joke, wasn't it?
Thanks for posting! I missed the original posting, but this pattern looks handy. Since you asked for feedback I decided to read through and give some thoughts.
There's a couple of other Nim projects you might be interested in and could give you insights:
Your code seems reasonable but still very C-like. Not a bad thing, but you do miss a fair bit of chances for better typing!
One quick aside first. You can use your <-- >-- macros as prefix operators, which might be easier to read:
>-- Message(...)
<-- Message(...)
The big thing I see is that while your StageMachine type is inheritable, you don't take advantage of it. For inheritable objects Nim lets you do a safe runtime conversion of types, but it works best with ref object. Also, ref offers a lot of advantages over raw ptr.
For example you have the types:
type
StageMachine* {.inheritable.} = object
type
WorkerMachine = object of StageMachine
ListenMachine = object of StageMachine
For example if you change them to ref object and ref object of StageMachine types you can then get ride of the ptr StageMachine in the rest of your code. It looks like 90% of your usages are ptr types so it'd be cleaner.
Then you can do nice things like:
import strformat
type
StageMachine* {.inheritable.} = ref object
stage*: string
OtherObj* {.inheritable.} = ref object
name*: string
type
WorkerMachine* = ref object of StageMachine
wmId*: int
ListenMachine* = ref object of StageMachine
lmId*: int
EnterFunc = proc(me: StageMachine)
var cb: EnterFunc
proc addProc[T: StageMachine](foo: proc(me: T)) =
## it's easiest to just wrap it but you could create a macro
## or template that did this inside foo without overhead
cb = proc(me: StageMachine) =
if me of T: foo(T(me)) else: echo "warning: skipping wrong type"
## Example Usage
proc receptionInitEnter(self: ListenMachine) =
echo fmt"reception: enter => {self.lmId=} {self.stage=}"
let
lm = ListenMachine(stage: "b", lmId: 456)
wm = WorkerMachine(stage: "b", wmId: 123)
addProc(receptionInitEnter)
proc runProc[T: StageMachine](me: T) =
cb(me)
## works
let m1: StageMachine = lm
runProc(m1) # => "reception: enter => self.lmId=456 self.stage=b"
## allowed but just prints warning
let m2: StageMachine = wm
runProc(m2) # => "warning: skipping wrong type"
## the compiler prevents this
let nonMachine: OtherObj = OtherObj(name: "non-esm")
assert not compiles(runProc(nonMachine))
You could also do things like make StageMachine generic or to use variant types, etc.
Note, when using a ref object you'll need to be a bit careful sending it between threads. It's best to use ARC/ORC with move, and there's a few details to be careful on, but overall it's pretty easy.
type
WorkerMachine* = object of StageMachine
WorkerMachineRef* = ref WorkerMachine