Behavioral Programming in Clojure

Behavioral Programming is a relatively new programming paradigm that excels at isolating and composing behaviors in event driven system. It is unrelated to behavior driven development.

Behavioral programming was invented by David Harel, who also invented statecharts in 1987. It uses independent units of behavior, called bthreads, which are coordinated in a pub-sub protocol.

How does it work?

A behavioral program will first collect the bids from all of its bthreads. A bid can do three things:

  1. Request events
  2. Wait on events
  3. Block events

The behavioral program will then consider all the requested events which are not blocked by any threads. It will then select a winning bid. Often this is done by assigning priorities to the bthreads, although this is not a requirement.

Having selected the winning bid, the program will then notify all the bthreads that have requested that event, or are waiting on that event. Those notified bthreads will then submit new bids. The process will continue until all bthreads are waiting.

Achiya Elyasaf’s Context-Oriented Behavioral Programming has this helpful diagram:

bprogram-lifecycle

A behavioral programming library, BPJ, exists for Java.

Let’s look at a stock example, from the article Behavioral Programming by David Harel, Assaf Marron, and Gera Weiss (2012). Suppose we are building industrial controls for a very simple system. We want three gallons of hot water and three gallons of cold water.

class AddHotThreeTimes extends BThread {
  public void runBThread() {
    for (int i = 1; i <= 3; i++) {
      bp.bSync( addHot, none, none );
    }
  }
}

class AddColdThreeTimes extends BThread {
  public void runBThread() {
    for (int i = 1; i <= 3; i++) {
      bp.bSync( addCold, none, none );
    }
  }
}

Great! This results in six gallons of water.

The bp.bSync call takes three arguments: an event (or multiple events) to request, an event (or events) to wait for, and an event (or events) to block.

A program with the bthreads AddHotThreeTimes and AddColdThreeTimes will do just what they say. This will result in three units of hot water and three units of cold water.

But what if a new requirement emerges: we need to alternate between hot and cold water to control the temperature?

We could mush the two behaviors together into a single method. Or we could create a higher-level method directly coupled to the two original methods. But what if we could incrementally and simply add behaviors?

class Interleave extends BThread {
  public void runBThread() {
    while (true) {
      bp.bSync( none, addHot, addCold );
      bp.bSync( none, addCold, addHot );
    }
  }
}

Behavioral programming gives us the ability to incrementally add independent behaviors. Our two previous bthreads do not need to know about each other. And our new behavior needs to know nothing directly about those bthreads!

The new bthread only defines the following behaviors, using events as an abstraction:

  • Do not add hot water until cold water has been added: (bp.bSync( none, addHot, addCold ))
  • Then, do not add cold water again until hot water has been added: bp.bSync( none, addCold, addHot )

Notice the radical simplicity at the level of behavior. Interleave only cares about events. It does not need to know who or what is responsible for adding cold or hot water. It simply describes a behavior.

Behavioral programming lends itself well to visualization. The following example is taken from A Review of Behavioral Programming, by David Harel, Assaf Marron, Smadar Szekely, and Gera Weiss.

add-hot-and-cold-viz

The Java implementation is imperative. Its bthreads use real system threads.

I believe that the simplicity of behavioral programming is an excellent fit for Clojure, and Clojure lends itself well to a nicer way of doing behavioral programming.

Bthreads in Clojure

Let’s port our water example to Clojure. We will use my proof of concept library, pavlov.

(ns water-controls.app
  (:require [tech.thomascothran.pavlov.bthread :as b]
            [tech.thomascothran.pavlov.bprogram :as bp]))

(def water-app
  (let [add-hot  (b/seq (repeat 3 {:request #{:add-hot-water}}))
        add-cold (b/seq (repeat 3 {:request #{:add-cold-water}}))
        alt-temp (b/seq 
                    (interleave
                       (repeat {:wait-on #{:add-cold-water}
                                :block #{:add-hot-water}})
                       (repeat {:wait-on #{:add-hot-water}
                                :block #{:add-cold-water}})))]
    (bp/make-program [add-hot add-cold alt-temp]))

Bthreads can be created from sequences with b/seq. They do not map onto real threads. For more general behavior, we have a b/reduce function which takes an event, and returns a bid and any additional information when the next event is triggered. For example:

(defn only-3-times
   [{:keys [times-called]} _]
   (when-not (= times-called 3)
     {:request #{:more}
      :times-called (inc times-called)})

When the bthread returns nil, it terminates.

Why Behavioral Programming?

Because bthreads are independent, a thread pool can be used to run them in parallel. Because they are cheap (in most cases, they will be waiting most of the time), you can create many of them.

Behavioral programming can be used to build very sophisticated behaviors from very simple building blocks. More interesting examples exist, ranging from aircraft control systems, to robotics, to nanosatellites. Behavioral programs lend themselves to formal verification and to visualization. Behavioral programming has a sound theoretical foundation.

There is much more to behavioral programming. For example, behavioral programs can use adaptive learning. They can be trained through a GUI. Bthreads can create other bthreads. Time can be introduced as a first class citizen.

Most importantly, they introduce a simple, declarative paradigm for incrementally adding independent behaviors.

For further reading on behavioral programming: