React-behavioral

Luca Matteis
Oct 12, 2018 · 8 min read

A React library aligned with how humans think

#BehavioralProgramming is a paradigm that was coined by David Harel, Assaf Marron and Gera Weiss (paper).

It’s a different way of programming that is more aligned with how people think about behavior. Specifically we program using these software modules called b-threads (aka function generators) that run in parallel and can request, wait and block events.

react-behavioral is a library, specifically targeted at React, that implements this paradigm.

TicTacToe Example

To understand better how BP (short for Behavioral Programming) works let’s imagine that we wanted to teach a person how to play the TicTacToe game.

We’d first start by showing the board to the person and we’d tell them that we can draw Xs and Os on this board. Try clicking on the board yourself; shift-click to draw Os. On the right we have a chronological view of the log of events as they happen.

Every time a cell on the board is clicked a new event is triggered that looks something like:

{ type: 'X', payload: 3 }

Where type can be either X or O and the payload is a number between 0 and 8 representing a cell's position.

Please check this CodeSandbox if you’re interested in learning about how each component works.

DetectWins

Let’s continue teaching the human (or computer) about how the game should work. We want to detect when a line of Xs or Os has been filled, and request a Win. Essentially we’re looking for a trace of events of this kind:

{ type: 'X', payload: 0 }
{ type: 'X', payload: 1 }
{ type: 'X', payload: 2 }

We’ll start coding our first b-thread which is aligned with our requirements which is called detectWinByX:

function* detectWinByX() {
  yield {
    wait: ['X']
  };
  yield {
    wait: ['X']
  };
  yield {
    wait: ['X']
  };
  yield {
    request: ['XWins']
  };
}

Here were are introducing the 3 main critical pillars of Behavioral Programming; the request, wait and block semantics.

A b-thread can yield an object with a key being any of these 3 operators. When the key is wait, the execution of the generator will stop at that section until such event is triggered by other b-threads or by the outside world.

When a request is yielded, the behavioral system will try to trigger such event. I highlight the “try” because a request is only a proposal for that event to be considered for triggering.

Finally, and more importantly, when a block is yielded, other b-threads are forbid to trigger such event.

These 3 magical operators can also be combined together; one can yield a block and a wait together, effectively “blocking until another event happens”. Or one can yield a request with a wait, effectively aborting the request when the wait event happens.

There are other semantics surrounding these 3 operators and they are crucial to the functioning of Behavioral Programming, hence the official image from the paper is needed:

Let’s go back to our detectWinByX b-thread and analyze how it’s simply waiting for three X’s in a row and then requests XWins. Try drawing 3 X’s on the board below:

You’ll notice that from the b-thread defined earlier, an XWins event is triggered once the trace is found. However the detectWinByX thread is incomplete. To detect a win we can’t simply listen out for any 3 X events, they need to form a line. More specifically in fact we need to create 8 threads, each of which listen out for a specific line and then declare a win:

...generateThreads(
  allLines,
  ([cell1, cell2, cell3]) =>
    function* detectWinByX() {
      const eventFn = matchAny('X', [cell1, cell2, cell3]);
      yield {
        wait: [eventFn]
      };
      yield {
        wait: [eventFn]
      };
      yield {
        wait: [eventFn]
      };
      yield {
        request: ['XWins']
      };
  }
)

With a little help from some utility functions we can now see the desired outcome. I’ve also added the same b-threads to trace O lines and trigger an OWins event when it happens. Try it out below:

Feature 1: behavioral autonomy.

Behavioral programming makes it possible to encapsulate the implementation of each behavior requirement in an autonomous software object. For example, the above b-threads deal with winning conditions only, and are not concerned with issues irrelevant to that purpose (such as the enforcement of alternating turns, or the choice of an opening move). (paper)

EnforcePlayerTurns

So far so good. We haven’t done anything extremely mind-bending. Next we’ll continue teaching the computer how to play TicTacToe.

We will now add the enforcePlayerTurns b-thread, which is again aligned to the requirements: we should not be able to trigger two X’s or two O’s in a row. In other words, when X goes, only O is allowed after it and so on. This is how the game works!

function* enforcePlayerTurns() {
  while (true) {
    yield { wait: ['X'], block: ['O'] };
    yield { wait: ['O'], block: ['X'] };
  }
}

Here is where things start getting really interesting. Behavioral Programming allows us to shape the behavior of our program in an incremental fashion. As new ideas and requirements are discovered, we can forbid certain things from happening by simply adding new b-threads; without having to dig and figure out how earlier-written code works!

This is really huge and in fact I believe game-changing, so I’ll curb the excitement for now and show you what the game looks like now that we added this b-thread:

Try adding two X’s in a row. It will not work. It’s waiting for an O, and once that is triggered it waits for an X and so on.

StopGameAfterWin

We notice as we play the game that after a win is announced we can still play the game. This can easily be fixed again by adding a new b-thread (incrementality 😜):

function* stopGameAfterWin() {
  yield {
    wait: ['XWins', 'OWins']
  };
  yield {
    block: ['X', 'O']
  };
}

Feature 2: positive and negative incrementality

In addition to adding new behaviors, and thus helping build up the desired dynamics of the system, a b-thread can constrain the dynamics by forbidding unwanted behaviors. This ability allows programmers to “sculpt” the system under construction, adding, removing or suppressing behaviors, with little or no need to modify the code of existing b-threads.

Programming the strategy

So far we programmed the basic rules for the game. We shaped and sculpted the behavior incrementally, in a way that didn’t require us to go back into already-written code to figure out state-based cases and change them accordingly to accommodate the new behavior.

Instead we programmed in a scenario-based fashion (a scenario being a b-thread) which seems to be much more natural. As a consequence of this, state is actually implicit rather than explicit; not once we had to worry about iterating over cells or peeking at the current state.

Next we’ll move into programming strategy for the game: we want the computer to play with us.

DefaultMoves

For a second let’s imagine that we forgot entirely about all the code that we have written so far. Or we can imagine that this code was written by other people. We don’t care. We need to update our game as new requirements come in. This new requirement seems like a tuff one, they want the O’s to appear automatically as the user positions X’s on the board.

const defaultMoves = generateThreads(
  allCells,
  cellNumber =>
    function*() {
      while (true) {
        yield {
          request: ['O'],
          payload: cellNumber
        };
      }
    }
);

Here we are creating a b-thread for each cell, where each is simply requesting to draw an O in such cell location. We’re essentially trying to draw O’s on the board, constantly.

Below we see the game with the above b-thread. Try playing the game, you’ll magically see the O’s appearing on the board. In the right positions!

StartAtCenter

New developers come on board, they start playing the game, they see the event trace, but they want the O’s to start at the center of the board (position 4). Normally we’d have to dig into already-written code to figure something like this out, possibly changing behavior inadvertently. But with BP everything seems easier:

function* startAtCenter() {
  yield {
    request: ['O'],
    payload: 4
  };
}

For this b-thread to work we need to introduce the final pillar element of Behavioral Programming:priority. Each b-thread has a specific number attached to them which essentially dictates which event should be requested in the case several b-threads are requesting the same event. In this case the O is also being requested by the defaultMovesthread. Hence we need to give startAtCenter a higher priority. Here's the final result:

Feature 3: early/partial execution

Initial or partial versions of behavioral programs can be executed and can often display meaningful system behavior. This allows users and developers to observe the system’s behavior at any stage of development, even the earliest ones, and take necessary actions, such as confirming that requirements are met, adjusting requirements, correcting errors, etc.

PreventCompletionOfLineWithTwoXs

Finally to make things a little more interesting we want the computer to be a bit smarter rather than simply putting O’s on the board in order from top-left to bottom-right.

We want to have a b-thread observe when the user has drawn two X’s in a line and then forcibly request an O (again with higher priority) so that it stops X from winning.

const preventCompletionOfLineWithTwoXs = generateThreads(
  allLines,
  ([cell1, cell2, cell3]) =>
    function* () {
      const eventFn = matchAny('X', [cell1, cell2, cell3]);
      let line = [cell1, cell2, cell3];      // Wait for two X's
      yield {
        wait: [eventFn]
      };
      line = line.filter(n => n !== this.bp.lastPayload);
      yield {
        wait: [eventFn]
      };
      line = line.filter(n => n !== this.bp.lastPayload);      // Request an O
      yield {
        request: ['O'],
        payload: line[0]
      };
    }
);

This b-thread is a bit more beefy, but it’s essentially waiting for two X’s in a line. It records their payloads (the cell position) and then knows to request an O in the last cell in the line. Give it a shot, the computer just got smarter!

Conclusion

How does all this tie into UI-development and React? First of all Behavioral Programming is concept that can be applied to any reactive system ranging complicated robotics to simple UI development. Secondly, react-behavioral goes a step forward and makes a React component also a b-thread 😮.

Matter of fact, each cell in the above example is a React component which looks like this:

function* ReactCell() {
  const { idx } = this.props;
  this.updateView(
    <button
      onClick={e => {
        if (e.shiftKey) {
          return this.request('O', idx);
        }
        this.request('X', idx);
      }}
    />
  );
  const eventFn = (event, payload) =>
    (event === 'X' || event === 'O') && payload === idx;
  yield {
    wait: [eventFn]
  };
  this.updateView(<button>{this.bp.lastEvent}</button>);
  yield {
    block: [eventFn]
  };
}const Cell = connect(ReactCell);

And then we can simply render using a Provider, which uses React’s Context to pass events around:

<Provider
  threads={[
    ...threads1,
    enforcePlayerTurns,
    stopGameAfterWin,
    ...preventCompletionOfLineWithTwoXs,
    startAtCenter,
    ...defaultMoves
  ]}
>
  <Wrapper>
    <Board>
      <Cell idx={0} /> <Cell idx={1} />{' '}
      <Cell idx={2} /> <br />
      <Cell idx={3} /> <Cell idx={4} />{' '}
      <Cell idx={5} /> <br />
      <Cell idx={6} /> <Cell idx={7} />{' '}
      <Cell idx={8} /> <br />
    </Board>
    <Log />
  </Wrapper>
</Provider>

I hope to have sparked some interest into this new way of programming. If you want to read more about it I highly suggest you read academics paper about it on google scholar “behavioral programming david harel”.

Also please checkout my slides on a talk I gave about these concepts here.