@@ -298,6 +298,141 @@ See
298298[ this SciPy tutorial: loopy programming] ( https://github.com/jpivarski-talks/2022-07-11-scipy-loopy-tutorial/blob/main/narrative.ipynb )
299299for more about array based languages & NumPy.
300300
301+ ## Reactive programming (signals and slots)
302+
303+ Another pattern, used heavily in GUI (graphical user interface) programming is
304+ signals and slots. (You'll also see this with JavaScript, as web interfaces are
305+ similar.) GUI toolkits tend to be harder to install than most libraries, so a
306+ good way to play around with this is via ` textual ` , which is a TUI (terminal
307+ user interface) library, which works most places except WebAssembly. However,
308+ there's now a library, ` reaktiv ` , which just implements the signal/slots
309+ paradigm without having a user interface; that's a great way to play with this
310+ for learning.
311+
312+ The core of this pattern is a callback, which is a function that gets registered
313+ to be run at a later time. Here's an example of a callback in Python:
314+
315+ ````` {tab-set}
316+ ````{tab-item} Sync
317+ ```python
318+ import threading
319+
320+
321+ def delayed_callback():
322+ print("Callback fired after delay!")
323+
324+
325+ timer = threading.Timer(2.0, delayed_callback)
326+ timer.start()
327+ print("Waiting...")
328+ ```
329+ ````
330+ ````{tab-item} Async (notebook)
331+ ```python
332+ import asyncio
333+
334+
335+ def delayed_callback():
336+ print("Callback fired after delay!")
337+
338+
339+ loop = asyncio.get_running_loop()
340+ loop.call_later(2, delayed_callback)
341+ print("Waiting...")
342+ ```
343+ ````
344+ `````
345+
346+ This will print:
347+
348+ ``` output
349+ Waiting...
350+ Callback fired after delay!
351+ ```
352+
353+ We've registered a function to run 2 seconds later, then moved on with our
354+ program. If you are in an environment that doesn't support threads (like
355+ WebAssembly), you'll have to use an async version of the above example to avoid
356+ threads.
357+
358+ Here's another example that does work in WebAssembly:
359+
360+ ``` python
361+ import argparse
362+
363+
364+ class PrintMessage (argparse .Action ):
365+ def __call__ (self , parser , namespace , values , option_string = None ):
366+ print (f " Callback triggered! You passed: { values} " )
367+ setattr (namespace, self .dest, values)
368+
369+
370+ parser = argparse.ArgumentParser()
371+ parser.add_argument(" --say" , action = PrintMessage)
372+
373+ # Simulate command-line input, but without leaving the notebook
374+ args = parser.parse_args([" --say" , " Hello from argparse!" ])
375+ ```
376+
377+ Ignore the fact that argparse requires a class with a ` __call__ ` instead of a
378+ normal function, and passes a lot of stuff in, and that you are supposed to
379+ manually handle the setting of the namespace with the value if you use it. The
380+ core idea is that we've defined what to do when an argument is passed in a
381+ function, and that function gets run (only) when the argument is passed.
382+
383+ Now let's try setting up our own reactive code. Our goal will be to be able to
384+ register callbacks (slots) that we can then trigger when emitting a signal.
385+
386+ ``` python
387+ @dataclasses.dataclass
388+ class Signal :
389+ slots = dataclasses.field(default_factory = list )
390+
391+ def connect (self , slot ):
392+ self .slots.append(slot)
393+
394+ def emit (self , value ):
395+ for callback in self .slots:
396+ callback(value)
397+
398+
399+ sig = Signal()
400+ sig.connect(lambda x : print (f " Got update: { x} " ))
401+
402+ sig.emit(42 )
403+ ```
404+
405+ When we call ` sig.emit ` , then all the callbacks we've registered as slots will
406+ run.
407+
408+ Here's an example using the ` reaktiv ` library, so we don't have to deal with the
409+ setup, and we can start seeing some of the benefits. If you are in WebAssembly,
410+ use ` import micropip; await micropip.install("reaktiv") ` to install.
411+
412+ ``` python
413+ import reaktiv
414+
415+ name = reaktiv.Signal(" Alice" )
416+ age = reaktiv.Signal(30 )
417+
418+ greeting = Computed(lambda : f " Hello, { name()} ! You are { age()} years old. " )
419+ greeting_effect = Effect(lambda : print (f " Updated: { greeting()} " ))
420+ # Updated: Hello, Alice! You are 30 years old.
421+
422+ name.set(" Bob" )
423+ # Updated: Hello, Bob! You are 30 years old.
424+ age.set(31 )
425+ # Updated: Hello, Bob! You are 31 years old.
426+ ```
427+
428+ Here, the Effect (callback) runs any time the signals connected to greeting are
429+ emitted, which they do when the value stored is changed.
430+
431+ In a GUI/TUI framework, pretty much every possible interaction with the
432+ interface (like clicking on a button, moving the mouse over a button, pressing a
433+ key, etc) emits a signal. You can connect slots in order to control what
434+ happens.
435+
301436## Memory safety
302437
303438### Garbage collected languages
0 commit comments