Focus on what matters: use managed-state-event-loop


#1

This micro-framework fell out of my Trinkey work and is super-helpful. It allows you to schedule stateless functions to run on a recurring basis while handling all the input/output without resorting to global storage. You can even share state across functions.

Check it out:


#2

I’m looking for a method of scheduling tasks that don’t require the use of a busyloop or delay. How are folks managing this in their code currently without interrupts?

For example, say you want to set a pomodoro timer, that notifies you when 45 minutes have past, and then go back to editing a function/document in the uLisp editor. How would that be accomplished as minimally as possible?

I think one could create a C function (that runs after the repl function in the main loop) which checks a global association list with the time as the key and the function as the value. The function would need to be a thunk, and have all of it’s parameters supplied by the calling function.

Any other ideas?


#3

You’re getting into OS like functionality here. There’s the two kinds of multitasking; preemptive and cooperative. uLisp doesn’t provide preemptive multitasking functionality afaik. Otoh, cooperative can be really good because you can have coroutines and closures pretty easily in uLisp. Then you’d want a framework to call them on time, which would be something like what the OP posted. I usually go with the latter style, but I haven’t done anything involving scheduling in uLisp surprisingly.


#4

Yes, and no. Yes, I would like to extend the “LispMachine” concept to be a functional lisp OS. But mostly I’m trying to work around the limitations of not having the event driven capabilities that are built into most languages. In bare metal microcontroller C, I would have interrupt handling, same with Scheme or Common Lisp.

The functionality I’m looking for is not much different than adding break to a running REPL, and evaling a thunk, and then returning to the original REPL eval. Or trampolining between two tail call optimized recursive functions, one of which is checking for a specific event, and then finally running the tail call (thunk) when it evaluates true.

Any ways, I was just wondering if anyone had a method of getting around this limitation in uLisp. We have millis, for-millis, and delay, but no way of concurrently waiting without locking up the REPL with a busy-wait.


#5

I thought a bit about this. I believe a very simple kind of cooperative multitasking could be done using a scheduler as the main infinite loop in the repl and encapsulating all other functionality within closures. This should save the context of the “tasks” and make them independent. The drawback would be that all functions of a task need to sit within that closure - but for smaller programs, why not? For larger programs this might create blocks that are hard to read, though, but the overhead would still be rather small.
Another thing I’ve been thinking of is the timer interrupt functionality of processors like the 2350. It should easily be possible to extend uLisp with small functions that set a global “time’s up” flag whenever the timer interrupt fires. This could be used by cooperative tasks as some kind of signal it’s time to give way.
All that would need to be strictly cooperative since there would be no means of protection (apart from variables within closures), and the whole idea obviously fails as soon as blocking functions like (delay) are used.
Bottom line: I think it’s possible and useful for several purposes, but on the other hand there’s not that much difference to a “main loop” that calls functions whenever a timespan measured with “millis” has passed. A scheduler as described would have very restricted means of control.

But then again I don’t think it’s a bad thing to circumvent a real OS, because IMHO this quickly leads to some kind of hard to track black box behaviour/annoying abstraction levels and the usual bureaucracy overhead I’m glad -not- to be forced to deal with using uLisp.


#6

You can see what I did when I attempted my operating system experiment uos uLisp Operating System as far as keeping programs inside closures and passing control between various applications. I don’t think it would be too hard to add a scheduler to that framework, but I could be naive. The whole difficult aspect is writing everything else in a manner that cooperates with the framework.

My ultimate goal was to write a much more full featured REPL (than the default uLisp one), but it was surprisingly hard, even harder than writing an editor. But once you have a REPL that can support getting “interrupted”, the scheduling part should be easy afaik (just call millis every time you check for a keypress and compare it to a list of tasks, or whatever scheduling algo you want to use).


#7

Yes, I think adding a scheduler to the REPL would be require doing continuation passing style evaluation, where each lambda has a context that it has captured responds to instead of one ENV stack for eval. Continuations are often done with lexical closures, so perhaps this is what you’re talking about. Sadly the term closure is a bit overloaded in math and functional programming.

A simpler method may be to have a C function, that is called before or after the REPL function’s call in the main loop, which checks a task/job queue to see if it’s time has come up, and then either runs it directly or drops it into the REPL to run next. The later would allow for “interactive” expressions that require feedback from the user to run, as opposed to just running thunks that update global variables alone.

This would of course require that the REPL has finished running, so infinite or long running programs would block the scheduler in similar ways that garbage collection is blocked, and has to be called manually. This wouldn’t be much of a problem for multi core micros such as the ESP32, which run FreeRTOS, as you could just pin the scheduler to a RTOS Task on another core.

New scheduled tasks could be added to the task queue by passing the time and function to run similar to the above managed state event loop.


#8

I feel like you might want to give a more specific example of how exactly this should play out. Like when you say “For example, say you want to set a pomodoro timer, that notifies you when 45 minutes have past” how exactly would the notification work? Will it write something to a screen? or a buffer? Does it need to share any data with the REPL? Does the user need to confirm it to continue using the REPL?

Because if its something like a popup, you’d want something resembling a windowing system for example, and then said windowing system could be responsible for how state is stored in various windows. Otoh if it just prints a message in the REPL then you might not need to save state at all.


#9

I’m actually trying to send out LoRA and BLE radio broadcasts at different allotted times, but that is irrelevant. There are a variety of use cases, the pomodoro timer being a simple one. If you wanted to set up a notification to pop up on the window, you’d still hope that the window manager doesn’t have to block the REPL that is running elsewhere.