An intro to Behavioral Programming
Software systems are built nowadays in a way that seem relatable to a constant experiment. Because software is malleable and quickly updatable, a characteristic not quite shared with hardware, we have the freedom to experiment and update this artifact we call “code” when there are new requirements.
Requirements constantly change: the artifact will change and grow in complexity and is always refined based on new user-based necessities. Something developed with high-use of “over-engineering” might not be the best solution since such change may need to be validated by users first and thrown out if not needed.
However, our current programming mentality only in part seems to address this demand for easy-adjustment of this artifact we call code. We have developed design patterns, paradigms and higher-level programming languages that make software much easier to maintain; but is it really easier to update?
I’d argue that even with the advent of new technologies and idioms our programming mindset is still the same since programming was invented.
Updating code requires us to change parts of the artifact in a way where we have to look at different section of the code, understand how those work and what they mean, add new bits and pieces, or alter sections all-together.
Below is an example of how software is updated:
This to me seems to be the “bottleneck” of software-development: we need to understand the context in which the new pieces are added and whether the modification will cause earlier parts to break.
In essence we are somewhat forced to maintain and understand a complex artifact in order for us make changes to the system.
You might be thinking: how else would we go about changing software?Remember, our goal is to find a way that allows for easier-adjustment.
Since the bottleneck seems to be the artifact, what if we didn’t have access to the artifact itself? What if the only way to make changes to the system was by adding new code? Then we wouldn’t have to read and understand where to squeeze our changes. We’d simply add stuff based on the new requirements and things would somehow magically work.
Let’s dig into this idea of append-only development.
Here I introduce the concept of b-threads which arise from a paradigm called “Behavioral Programming” (BP). Below is an image of how they work.
B-threads are essentially function generators that run in parallel. They do mainly 3 things: they can request, wait or (uniquely to BP) block events.
Let’s take a closer look at how b-threads are executed using a Behavioral Programming system. Below we execute a simple b-thread called “add hot 3 times when the water-level is low”:
B-threads yield objects which contain keys representing what they should be doing. When there’s a wait statement, the system will wait for such event to happen before it continues.
Let’s now add a new b-thread that will “add cold 3 times when the water-level is low”:
The b-thread from before remains untouched. We added a new one to the right. For now there’s nothing surprising. The behavior of this program (compromised of these 2 b-threads) is that whenever “waterLevelLow” happens, we add hot 3 times and then add cold 3 times.
Let’s now dive into what I think is the most interesting feature of BP, namely the idea of incrementality: changing the behavior of a system without changing or even seeing how old code is implemented.
In this example we add another b-thread called “stability” (again we’re simply appending it to the right of the existing b-threads):
Every time the yellow line changes its position (when a b-thread goes to its next yield statement) we check all the yield statements along the yellow line across all b-threads. This is why the yellow line “crosses” through all b-threads.
If there’s a b-thread that is blocking an event, other b-threads cannot trigger it and hence cannot continue their execution.
In this example our newly-added b-thread is blocking “addCold” until “addHot” is triggered. After which it does the opposite.
At each step the green squares show the “selected event”. The red boxes show which event is blocked.
As can be observed by the event trace (which can be thought of as the output of the program) we are effectively changing the behavior of the system without changing old code.
This idea of having new code dictate what should happen or what shouldn’t happen using this imaginary yellow line is what’s going to drive the general intuition of this article.
As a mental exercise let’s imagine a system where we have no access to the source-code that composes it. The only way to understand what the system does is through its output: the event trace.
In order to change the system based on new requirements we are only allowed to append code, specifically b-threads; similar to how we did in the hot/cold example from earlier.
Below we have an example of such system as an ATM:
As things happen to the ATM we see its trace. For instance, as the user inserts the card, we see “cardInserted” happen. As the account loads we see “loadingAccount” etc.
A new requirement that needs implementation is shown as an orange rounded box at the top: “Show advertisement before the account is loaded”.
Because we don’t have access to the source-code, we must think of implementing this change only through the event trace. Adding, removing or more generally coordinating when events should happen in the trace is how we’re going to implement new requirements.
In the above example we see where the “showAd” and “adShown” events should appear in the event trace; namely before “loadingAccount” (since that’s what the new requirement tells us).
But how we can add these two events at this particular section of the event trace without access to the source-code? Well, using b-threads and the coordination system of the imaginary yellow line we can do this quite naturally:
We are essentially pushing these two b-threads into the ATM system (again without knowing or caring about how the current code is written or implemented).
The b-threads coordinate this new requirement by waiting for “cardIsValid” which currently happens before “loadingAccount”.
Once “cardIsValid” happens, the left b-thread blocks “loadingAccount” until “adShown”. At the same time the b-thread on the right requests “showAd”, then actually shows the ad by calling the
showAdvertiments() function and finally requests “adShown”.
This final request of “adShown” allows the first b-thread to free up and finally trigger the “loadingAccount” event.
The output of all this is exactly implementing our requirement. And once again, we didn’t have to understand how old code was written to implement this change.
Alignment with requirements
As we continue from our earlier ATM example we can add new requirements in this append-only fashion. B-threads are “piled-a-top” looking much more similar in context to actual requirements. Usually requirements are spread across different code modules and functions, but b-threads are much more aligned to what the actual requirement entails.
A new requirement suggests to “not to show the advertisement if the user is enterprise”:
And again here’s how we implement it:
In the b-thread on the right the “loadingAccount” is freed up from being blocked when “isEnterprise” occurs. And on the left we simply block “showAd” when “isEnterprise” happens.
Pushing these two b-threads (or laying-them-a-top) into the ATM will generate the appropriate event-trace.
This yellow imaginary line that crosses through all of the b-threads and decides which event is triggered is actually called formally a synchronization point.
In a way b-threads are all coordinating what needs to happen from their own point of view and have no knowledge of what is happening in other b-threads.
We are used to implementing changes by “mixing” them into code which requires a much deeper understanding of the system:
Instead, using the Behavioral Programming system described so far, the requirements map quite naturally to how they exist in our minds:
As a system grows in complexity we don’t necessarily care about how old b-threads have been written, hence we don’t care about maintaining them. They can be thought of as a closed legacy system.
This leads us to a more general intuition of programming this way which is best described using an image from a great paper called “The quest for runware: on compositional, executable and intuitive models”.
Obviously this article cannot go much into all the details and research that has been made in this context. There other concepts such as priority, event-selection mechanisms, model-checking and much more that can be found in literature. One place to start is googling on scholar: “harel behavioral programming”.
In the context of frontend-development I have written some small experimental libraries that allow you to program this way:
- https://github.com/lmatteis/react-behavioral — An implementation that can be used directly with React (very experimental)
- https://github.com/lmatteis/redux-behavioral — Redux is an event-system where changes are described as traces. BP works very well with this system and hence this library.
Finally, there is other less technical articles I wrote and talks I gave on the subject which might spark your interest:
- https://lmatteis.github.io/react-behavioral/ — An interactive essay showing how b-threads work in the context of implementing a Tic Tac Toe game.
- “Moving” this imaginary line by requesting, waiting and blocking events allows for incremental development.
- Alignment with requirements and how humans think about behavior.
- B-threads are “piled-a-top” with no component-specific interface, connectivity or ordering requirements.
- Different way of thinking about programming.
- Ironically, more natural ways of programming are perceived as unnatural because of our past training.
- Does it scale with thousand or million concurrent b-threads?
- Not currently used by many people.
- Lack of best-practices, tools, community etc.