This is yet an early š beta š“ WIP š§ unstable attempt at implementing the thinking behind the paper Behavioral Programming by David Harel, Assaf Marron and Gera Weiss in Elm
For a more complete attempt, see react-behavioral by Luca Matteis.
So if you want to fiddle with behaviors at this very early stage, read below at your own risk. For now, please read the original article as well, so that the API makes a bit more sense.
A behavioral system is made of independent Thread
s (called B-threads in the original paper) of execution
that are agnostic of each other, but listen and react to an agreed-upon
set of system events. A thread can ask the system to do one of three things:
Request
a system event to be published. (Just a request, no guarantees)Block
an event from getting published.Wait
for an event and then change its state/behavior.
Since there are yet no proper communicating processes/threads in Elm,
I modeled threads as a series of functions that return a list of
Behavior
values to be interpreted by the system. Each one of these
functions is called a fiber, representing one contiguous block of
internal calculations before coming up with a new list of behaviors.
The system then evaluates all the behaviors as a whole, and runs the
next batch of fibers if there are Free
(ie. not Blocked
) events in
the requests. If there are no free events, the system returns its
internal State
, which can be re-invoked by external event
(side effect).
One or more of these fiber functions, chained together through their
returned behaviors, and possibly passing along some internal state, is
what makes up a (B-)Thread
Let's try the hot-cold water tap control example in the paper. One of the proposed advantages of behavioral programming is the ability to change system behavior without modifying the existing implementation, just by adding new behaviors.
Of course begin by importing the Behavior
module.
import Behavior exposing (..)
The first thing to do is define our common set of events for the system. Any data type in Elm can be used to describe an event, but getting the most out of the excellent Elm compiler, I recommend using an union type to define all the possible events.
type TapEvent
= Hot
| Cold
Not much here. Either a unit of Hot
water flows, or Cold
.
Now we need a thread to make that happen. Let's also give it some
internal state for the amount of water we want.
addHot : Int -> Thread TapEvent
addHot amount _ =
if amount > 0 then
[ request Hot [ addHot (amount - 1) ] ]
else
[]
Some points here
- The
_
in the function parameters is a stand-in for the empty tuple()
elm-behavioral will pass in to lazily invoke it. This is necessary to keep the compiler from crashing when you want to make a fiber recursively call itself indefinitely. - The returned list is the list of behaviors to evaluate at the next
sync point.
request
is the exposed constructor that takes an event to be considered for publishing, and a list of fibers to invoke when that event is finally published. Here,addHot
invokes itself with one less amount to go. - The
else
case returns an empty list[]
of behaviors, which practically means this thread has nothing more to do. A thread is over when it has no more behaviors, because it can't continue with any fibers.
We initialize
the system by giving it a list of fibers to begin with.
Here, we have just one thread. It immediately runs and evaluates all
the events it can. Once there are no more events that can be published,
it returns its State
, which contains the remaining fibers, and a log
of events that can be extracted by the function of the same name.
initialize [ addHot 3 ] |> log --> [Hot, Hot, Hot]
It is also possible to fire
events into the returned state.
This generates and injects a one-shot fiber that just requests
a single event.
initialize [ addHot 3 ]
|> fire Cold
|> log
--> [Cold, Hot, Hot, Hot]
Note that the last event is the first in the log. It works by prepending the events as they happen.
Let's just handwave the almost identical addCold
thread into existence and focus on a new requirement. The problem is this is what happens
when we try to get warm water.
initialize [ addHot 3, addCold 3 ]
|> log
--> [Cold, Cold, Cold, Hot, Hot, Hot]
We are first scalded by hot water and then frozen. š„µš„¶
So the new requirement is that we should be able to mix taps.
mix : TapEvent -> TapEvent -> Thread TapEvent
mix tap1 tap2 _ =
[ blockEvent tap1
, waitFor tap2 [ mix tap2 tap1 ]
]
Here is a new behavior that blocks one tap while waiting for the other, at which point it re-invokes itself with the inverse configuration.
initialize [ addHot 3, addCold 3, mix Hot Cold ]
|> log
--> [Hot, Cold, Hot, Cold, Hot, Cold]
š We can finally get a steady stream of water at the correct temperature.
For more examples, check out the examples folder and read the tests.