The Missing Manual for Signals: State Management for Python Developers
This triggered some associations for me.
Strongest was Cells[0], a library for Common Lisp CLOS. The earliest reference I can find is 2002[1], making it over 20 years old.
Second is incremental view maintenance systems like Feldera[2] or Materialize[3]. These use sophisticated theories (z-sets and differential dataflow) to apply efficient updates over sets of data, which generalizes the case of single variables.
The third thing I'm reminded of is Modelica[4], a language where variables are connected by relations (in the mathematical sense). So while A = B + C can update A on when B or C change, you also can update just A and B, then find out what C must have become.
[0] https://cells.common-lisp.dev
[1] https://web.archive.org/web/20021216222614/http://www.tilton...
What the author touches on with before and after "declarative thinking" is largely applicable to all Directed Acyclic Graph (DAG) workflows, and not just signals. They are 100% correct that there is a mental shift. Yes, you can use magic to implicitly declare your DAGs with signals. You can also be really explicit with dependencies.
DAG-based workflows incur a cost in terms of complexity, but there are a few advantages to using DAGs instead of sequential.
1. Parallelism becomes inherently built-in
2. It's easier for a new developer to understand the direct dependencies of a node on other nodes (compared to sequential). Sometime in the future, a developer may want to split off a task or move it up/downstream of other tasks.
3. Fault tolerance & recovery becomes easier. Just because 1 step fails, doesn't mean that the whole workflow must come to a halt.
How does this instance of Computed know that it depends on x? Does it parse the bytecode for the lambda? Does it call the lambda and observe which signals get accessed?y = Computed(lambda: calculate_y(x()))In my homebrew signal framework, which emerged in the middle of a complicated web dashboard, this would look like:
So the machinery gets to see the signal directly.y = Computed([x], calculate_y)One of the largest, if not the largest python codebase in the world, implements similar ideas to model financial instruments pricing: https://calpaterson.com/bank-python.html.
I've been writing front-end javascript the "just use functions" way and never really wanted to get into React because it looks too complicated. But this makes sense. God damn it I want to actually learn react now.
Two perplexing aspects:
- Why so many lambda functions instead of regular named functions? Is it a technical limitation? Something important should have a name, for instance (for the example in the article) different ways to compute greetings from names.
- How are the computations ordered, particularly multiple Effects that trigger at the same change? For instance, in the example in the article, when the name changes the updated greeting is printed before or after the updated location.
I've designed something like this before but in the context of orchestration. A 'read' implicitly became a subscription in the meta-process and any subsequent changes to the consumed value triggered updates to the consumer process that were either explicitly caught or handled through an implicit context process.
This allowed process definitions to remain simple enough for business analysts to comprehend while still able to cover real world complexity.
Hmm I have mixed feelings about this. I've thought about this topic for a bit, a couple of years ago I thought of bringing functional reactive programming to a backend node.js project (partially because of managing callback hell); in the past couple of years I work on an event/workflow system with 100+ million events per day.
This feels like a lighter weight alternative to Temporal or other workflow tools[0], but eventually for a backend system you'd likely be rebuilding features that are tailored for the backend.
In frontend code, you have many side effects (e.g. DOM, styling) that rely on a single piece of data/event, and more side effects that rely on those side effects (e.g. component hierarchy), and having this laid out declaratively is one way to understand the behavior of an application when this piece of data changes. You are also almost never concerned about durability/persistence of the state of data on the frontend, just because the code interacts with the browser and we almost never question the reliability of that API. A human is typically the "driver" of these interactions and is typically in the loop for most interactions, so stuff that fails, e.g. a failed network call, can bubble up to the user to deal with.
Conversely, web backend projects have code and infrastructure that are distributed (even monolithic ones), and most of the time are concerned with persisting state/data, distributing/scheduling workloads, etc. Each side effect / computation, especially ones that cross networks/service, has its own requirements for whether it should be at least once/at most once, retried/retry patterns, latency/throughput, failure modes/error handling. These requirements also define your boundaries/interfaces, rather than a nice semantic and declarative one (not exclusive but oftentimes the requirements win out).
Not saying that this signal-based approach can't be used in some areas of the application would benefit for declarative computation, but the examples given seem to indicate also a desire to do distributed workflow stuff.
[0] https://temporal.io/, https://github.com/meirwah/awesome-workflow-engines
I have a dream for a compiled reactive DSL for video game programming that makes replay and rollback netcode automagically, eliminates bugs in state management, and naturally expresses derived state and the simulation step/transition function, while still being performant enough for real time
The performance hit from all that indirection of registering, getters, setters, discover, traversal, and lambdas could be avoided if we could compile the dag into smartly nested ifs
For bigger workflows, this declarative pattern is already implemented by orchestrators like Dagster, Flyte, and recently Airflow; e.g., https://dagster.io/blog/declarative-scheduling [fixed]