Flapjax Revisited
Reactive programming for UI applications has been studied for a long time. But while the academic literature has focused mostly on functional reactive programming (FRP), which uses behaviors and events, the web has adopted a different approach: signals.
Why did signals win? Is this a case of worse-is-better or is there something deeper going on? I think there are some good reasons why signals won. In this post we’ll compare and contrast Flapjax, a JS FRP library, and Solid’s signals.
Note: This post assumes you are familiar with Solid’s signal system.
Flapjax Revisited
In 2009, Meyerovich et al. published “Flapjax: A Programming Language for Ajax Applications,” which argued for functional reactive programming for the web. But now, nearly 15 years later, functional reactive programming seems to have lost to signals. Why?
Let’s start by looking at the motivating example from the Flapjax paper:
var timerID = null;
var elapsedTime = 0;
function doEverySecond() {
elapsedTime += 1;
document.getElementById("curTime").innerHTML = elapsedTime;
}
function startTimer() {
timerId = setInterval("doEverySecond()", 1000);
}
function resetElapsed() {
elapsedTime = 0;
// bug! The DOM is not updated until the next call to doEverySecond
}
<body onload="startTimer()">
<input id="reset" type="button" value="Reset" onclick="resetElapsed()" /> <div id="curTime"> </div>
</body>;
This is the thought process the authors claim a developer must go through when reading this code:
- The value is ostensibly displayed by the second line of the function
doEverySecond
. - The value displayed is that of
elapsedTime
. elapsedTime
is set in the previous line.- But this depends on the invocation of
doEverySecond
. doEverySecond
is passed inside a string parameter tosetInterval
insidestartTimer
.startTimer
is called by theonload
handler… so it appears that’s where the value comes from.- Is that it? No, there’s also the initialization of the variable
elapsedTime
at the top. - Oh wait:
elapsedTime
is also set inresetElapsed
. - Does
resetElapsed
execute? Yes, it is invoked in theonclick
.
“Just to understand this tiny program,” they argue, “the developer needs to reason about timers, initialization,
overlap, interference, and the structure of callbacks.” Besides being complex, the program also contains a bug. When the user hits the reset button, the result isn’t propagated to the screen until the next call
to doEverySecond
.
Overall, I agree with this part of the argument. The code as written is hard to understand. The use
of code within strings makes it difficult to read, and the DOM mutations are hard to reason about.
The Flapjax authors go on to blame callbacks for this complexity. Specifically, they argue that
the fact that doEverySecond
, startTimer
, and resetElapsed
are all callbacks makes the code
hard to read. Are callbacks really the problem? Let’s see what this code would look like in Solid.
function App() {
const [elapsedTime, setElapsedTime] = createSignal(0);
const doEverySecond = () => {
setElapsedTime((elapsedTime) => elapsedTime + 1);
};
setInterval(() => doEverySecond(), 1000);
const resetElapsed = () => {
setElapsedTime(0);
};
return (
<>
<button onClick={() => resetElapsed()}>Reset</button>
<div>{elapsedTime()}</div>
</>
);
}
Does the same chain of reasoning apply?
- The value is ostensibly displayed by the second line of the function
doEverySecond
. No, the DOM mutation has been replaced by a combination of signals and JSX. - The value displayed is that of
elapsedTime
. Yes. elapsedTime
is set in the previous line. Yes.- But this depends on the invocation of
doEverySecond
. Yes. doEverySecond
is passed inside a string parameter tosetInterval
insidestartTimer
. No, it’s passed as a normal callback.startTimer
isn’t needed, because everything in the function body is called on load.startTimer
is called by theonload
handler… so it appears that’s where the value comes from. No.- Is that it? No, there’s also the initialization of the variable
elapsedTime
at the top. Yes. - Oh wait:
elapsedTime
is also set inresetElapsed
. Yes. - Does
resetElapsed
execute? Yes, it is invoked in theonclick
. Yes.
In my opinion, a more accurate reading of this function is:
- The value displayed is that of
elapsedTime
. elapsedTime
is initialized to 0.- By looking for
setElapsedTime
calls, we see thatdoEverySecond
andresetElapsed
updateelapsedTime
. doEverySecond
incrementselapsedTime
.doEverySecond
is called every second bysetInterval
.resetElapsed
resetselapsedTime
to 0.- The DOM contains a button that calls
resetElapsed
when clicked.
Notice also that the bug is not present in the Solid version, since the rendering logic is handled by JSX and the
elapsedTime
signal.
Let’s compare this to Flapjax’s solution. Flapjax uses behaviors (with the B
suffix) and event streams (with the E
suffix). “A behavior is like a variable – it always has a
value – except that changes to its value propagate automatically; an event stream is a
potentially infinite stream of discrete events whose new events trigger additional computation.” (To enhance readability, I renamed some of the variables that were shortened in
the paper.):
// a behavior that updates every second
var nowB = timerB(1000);
// a snapshot of the behavior’s value at the time it is invoked, i.e. it _does not_ update automatically
var startTime = nowB.valueNow();
// $E("reset", "click"): event stream of clicks
// snapshotE(nowB): converts the event stream into an event stream of behavior values sampled from nowB
// startsWith(startTime): converts event stream into behavior initialized with startTime
var clickTimesB = $E("reset", "click").snapshotE(nowB).startsWith(startTime);
// difference between behaviors. updated every time either behavior updates
var elapsedB = nowB - clickTimesB;
// insert value into the DOM
insertValueB(elapsedB, "currTime", "innerHTML");
<body onload="loader()">
<input id="reset" type="button" value="Reset" /> <div id="currTime"> </div>
</body>;
A behavior analogous to a signal, and an event stream is analogous to an event handler. To demonstrate this, we can convert the Flapjax example to Solid code. Every time we see a behavior in the Flapjax code, we’ll use a signal. Every time we see an event stream we’ll use an event handler.
// a signal that emits the current time every `interval` milliseconds
function timer(interval: number) {
const [timer, setTimer] = createSignal(Date.now());
setInterval(() => setTimer(Date.now()), interval);
return timer;
}
function App() {
// var nowB = timerB(1000);
const now = timer(1000);
// var startTime = nowB.valueNow();
// (this translates to just calling the signal directly rather than creating a derived signal)
const startTime = now();
// var clickTimesB = $E("reset", "click").snapshotE(nowB).startsWith(startTime);
// We don't define the event handler here. Notice that if we had more than one event handler,
// Flapjax would require us to define all of them up front and merge them together.
// This part actually covers:
// var clickTimesB = ....startsWith(startTime);
const [clickTimes, setClickTimes] = createSignal(startTime);
// var elapsedB = nowB - clickTimesB;
// Notice that elapsedB is a behavior, so it becomes a derived signal
const elapsed = () => Math.floor((now() - clickTimes()) / 1000);
// var clickTimesB = $E("reset", "click").snapshotE(nowB).startsWith(startTime);
// This line covers:
// $E("reset", "click").snapshotE(nowB)
const reset = () => {
setClickTimes(now());
};
return (
<>
<button onClick={reset}>Reset</button>
{/* insertValueB(elapsedB, "currTime", "innerHTML"); */}
<div>{elapsed()}</div>
</>
);
}
Notice the big difference between the Flapjax and Solid versions is where event streams/handlers are placed. Flapjax positions event streams locally to the behaviors they control. The advantage of this approach is that a behavior’s dependencies can be read directly off the code. However, it obscures the flow of control. Consider this line:
$E("reset", "click").snapshotE(nowB).startsWith(startTime);
It affords the following reading: “Reset button click events are converted into a stream of nowB
events, initialized with startTime
.”
The Solid version consists of these pieces of code:
const [clickTimes, setClickTimes] = createSignal(startTime);
const reset = () => {
setClickTimes(now());
};
<button onClick={reset}>Reset</button>
This code affords the following reasoning: “There is a signal called clickTimes
that is
initialized with startTime
. When the reset button is clicked, clickTimes
is updated with the
current time.”
Notice the subtle difference here. The Flapjax code is phrase as push-only. The button click flows
into the snapshot stream modifier, which flows into the initializer. But this doesn’t match the
actual data flow pattern which is push and pull. clickTimes
is initialized. The reset
event pushes an
update to clickTimes
by pulling now
. I think the Solid model matches my mental model of the
control flow a lot better.