Skip to content

Commit ee94d49

Browse files
committed
feat: add section on reactive programming
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 5010007 commit ee94d49

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

content/week07_design/designpatt.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
299299
for 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

slides/week-07-2.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,116 @@ In the above example, we compute the radius for plotting.
224224

225225
---
226226

227+
## Reactive programming (signals & slots)
228+
229+
Common in:
230+
231+
- GUIs (Qt)
232+
- TUIs (`textual`)
233+
- Spreadsheets
234+
235+
Based on the idea of a callback function.
236+
237+
---
238+
239+
## Reactive programming: callback (sync)
240+
241+
```python
242+
import threading
243+
244+
245+
def delayed_callback():
246+
print("Callback fired after delay!")
247+
248+
249+
timer = threading.Timer(2.0, delayed_callback)
250+
timer.start()
251+
print("Waiting...")
252+
```
253+
254+
---
255+
256+
## Reactive programming: callback (async)
257+
258+
```python
259+
import asyncio
260+
261+
262+
def delayed_callback():
263+
print("Callback fired after delay!")
264+
265+
266+
loop = asyncio.get_running_loop()
267+
loop.call_later(2, delayed_callback)
268+
print("Waiting...")
269+
```
270+
271+
---
272+
273+
## Reactive programming: callback (CLI)
274+
275+
```python
276+
import argparse
277+
278+
279+
class PrintMessage(argparse.Action):
280+
def __call__(self, parser, namespace, values, option_string=None):
281+
print(f"Callback triggered! You passed: {values}")
282+
setattr(namespace, self.dest, values)
283+
284+
285+
parser = argparse.ArgumentParser()
286+
parser.add_argument("--say", action=PrintMessage)
287+
288+
# Simulate command-line input, but without leaving the notebook
289+
args = parser.parse_args(["--say", "Hello from argparse!"])
290+
```
291+
292+
---
293+
294+
## Reactive programming: Signals and slots
295+
296+
```python
297+
@dataclasses.dataclass
298+
class Signal:
299+
slots = dataclasses.field(default_factory=list)
300+
301+
def connect(self, slot):
302+
self.slots.append(slot)
303+
304+
def emit(self, value):
305+
for callback in self.slots:
306+
callback(value)
307+
308+
309+
sig = Signal()
310+
sig.connect(lambda x: print(f"Got update: {x}"))
311+
312+
sig.emit(42)
313+
```
314+
315+
---
316+
317+
## Using the reaktiv library
318+
319+
```python
320+
import reaktiv
321+
322+
name = reaktiv.Signal("Alice")
323+
age = reaktiv.Signal(30)
324+
325+
greeting = Computed(lambda: f"Hello, {name()}! You are {age()} years old.")
326+
greeting_effect = Effect(lambda: print(f"Updated: {greeting()}"))
327+
# Updated: Hello, Alice! You are 30 years old.
328+
329+
name.set("Bob")
330+
# Updated: Hello, Bob! You are 30 years old.
331+
age.set(31)
332+
# Updated: Hello, Bob! You are 31 years old.
333+
```
334+
335+
---
336+
227337
## Memory safety: garbage collection
228338

229339
Garbage collected languages include most interpreted languages as well as C#, D, Java, Go.

0 commit comments

Comments
 (0)