BehaviorSubject is the thing that truly corresponds to Sodium’s Cell, having the concept of a current value. It starts off as a cold observable. When you subscribe, you get called back once immediately with the current value. Then it becomes a hot observable, giving you the updates as they come in.
For instance, figure 6.3 shows a program that allows you to select a dog or cat polygon. When you click OK, an alert pops up, saying “You selected cat” or “You selected dog.”
Figure 6.3 The Select application with the cat selected by default
Listing 6.3 line-stretch.js: Drawing a line from mouse-down to mouse-up position
Captures the most recent mouseDown position when mouseUp fires
Draws a cross at the mouseUp and mouseDown positions
On mouseUp, draws a line from the mouseDown to the mouseUp position
117 Keeping state in RxJS, Kefir.js, and Flapjax
Listings 6.4 and 6.5 show the Sodium hold equivalent, where you construct
var selected = new Rx.BehaviorSubject("cat");
sSelected.subscribe(selected);
The equivalent in Sodium would be
selected = sSelected.hold("cat");
The argument says that the cat is selected by default. Using this instead of using sSelected directly has two positive effects on the program:
■ It draws the scene on page load. Without the BehaviorSubject, you’d see noth- ing, because sSelected doesn’t fire until the user clicks the canvas.
■ If you only click OK, the program captures the default value (“cat”) that you passed to the BehaviorSubject. If you used sSelected directly, then if the user only clicked the OK button, nothing would happen. withLatestFrom() wouldn’t see any event on sSelected, so it would output nothing when it got an event on sOK.
<html><head>
<title>Select cat or dog then click OK - Rx.JS</title>
<style>
#myCanvas { border-style: solid; border-width: 1px } </style>
<script src="rx.all.min.js"></script>
<script src="select-rxjs.js"></script>
</head>
<body onload="init()">
<div>Select cat or dog then click OK - Rx.JS</div>
<canvas id="myCanvas" width="300" height="200">No canvas!</canvas>
<div><button id="ok">OK</button</div>
</body>
</html>
Forward references
The Observable.subscribe(BehaviorSubject) line can be written much later than the construction of the Rx.BehaviorSubject. This allows forward references, equiv- alent to CellLoop in Sodium. You can use this to write a state accumulator with a looped value, as you do in Sodium.
In a similar way, streams can be looped using a class called Rx.Subject(), giving an equivalent of Sodium’s StreamLoop:
var s0 = new Rx.Subject();
...
s.subscribe(s0);
Listing 6.4 select-rxjs.html: Select application for selecting polygons
Cell with a default value Changes it according
to sSelected
function insidePolygon(pos, poly) {
var x = pos.x, y = pos.y, coords = poly.coords, inside = false;
var v = coords[coords.length-1], x1 = v.x, y1 = v.y;
for( var i = -1; v = coords[++i]; ) { var x2 = v.x, y2 = v.y;
if( ( y1 < y && y2 >= y ) || ( y2 < y && y1 >= y ) ) if ( x1 + ( y - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) < x ) inside = ! inside;
x1 = x2, y1 = y2;
}
return inside;
}
var shapes = [
{ id: "cat", coords: [{ x:55, y:90 },{x:67,y:54},{x:72,y:89}, {x:99,y:88},{x:106,y:54},{x:115,y:91},{x:123,y:106}, {x:100,y:134},{x:88,y:130},{x:80,y:134},{x:48,y:108}]}, { id: "dog", coords: [{x:171,y:58},{x:154,y:80},{x:156,y:120}, {x:166,y:110},{x:166,y:82},{x:183,y:130},{x:202,y:127}, {x:221,y:78},{x:225,y:111},{x:237,y:119},{x:231,y:59}, {x:211,y:66},{x:195,y:60},{x:180,y:72}]}
]
function init() {
var canvas = document.getElementById("myCanvas");
var getXY = function(e) { return { x : e.pageX - canvas.offsetLeft, y : e.pageY - canvas.offsetTop }; };
var sMouseDown = Rx.Observable.fromEvent(canvas, 'mousedown') .map(getXY);
var sSelected = sMouseDown.map(function(pos) { for (var i = 0; i < shapes.length; i++) if (insidePolygon(pos, shapes[i])) return shapes[i].id;
return null;
});
var selected = new Rx.BehaviorSubject("cat");
sSelected.subscribe(selected);
var okButton = document.getElementById('ok');
var sOK = Rx.Observable.fromEvent(okButton, 'click');
sOK.withLatestFrom(selected, function(ok, sel) { return sel; }) .subscribe(function(sel) {
alert('You selected '+sel);
});
selected.subscribe(function(selected) { var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < shapes.length; i++) { var coords = shapes[i].coords;
ctx.beginPath();
ctx.moveTo(coords[0].x, coords[0].y);
for (var j = 0; j < coords.length; j++) ctx.lineTo(coords[j].x, coords[j].y);
ctx.closePath();
if (selected == shapes[i].id) { Listing 6.5 select-rxjs.js
“hold” a stream
119 Keeping state in RxJS, Kefir.js, and Flapjax
ctx.fillStyle = '#ff0000';
ctx.fill();
}
ctx.stroke();
} });
}
To run this example, point your browser at sodium/book/web/select-rxjs.html.
6.3.1 startWith() as shorthand for BehaviorSubject In the previous example, these two lines
var selected = new Rx.BehaviorSubject("cat");
sSelected.subscribe(selected);
could be written like this:
var selected = sSelected.startWith("cat");
This will give you an observable that reports "cat" once on any new subscription. In this program, the effect would be the same, but they aren’t equivalent.
On a new subscription, BehaviorSubject gives you "cat" if there haven’t been any events yet, or the latest value if there have been events. Because it remembers the lat- est change, it works like an FRP cell. But startWith always gives "cat" as the initial value on a new subscription, no matter how many events have gone before. It doesn’t remember the most recent event value like BehaviorSubject does. These two are only equivalent if you only ever subscribe to them during program initialization (before the first event is received) as many programs do.
6.3.2 The same again with Kefir.js
Now let’s do it again in Kefir.js (see listing 6.6). Kefir is based on RxJS, but the equiva- lent of Cell/BehaviorSubject is called Property. It behaves more like a distinct type, as in Sodium. toProperty is exactly equivalent to Sodium’s hold, and sampledBy is Sodium’s snapshot with the first two arguments reversed.
function init() {
var canvas = document.getElementById("myCanvas");
var getXY = function(e) { return { x : e.pageX - canvas.offsetLeft, y : e.pageY - canvas.offsetTop }; };
var sMouseDown = Kefir.fromBinder(function(emitter) { canvas.addEventListener("mousedown", emitter.emit);
return function() {
canvas.removeEventListener("mousedown", emitter.emit);
}
}).map(getXY);
var selected = sMouseDown.map(function(pos) { Listing 6.6 select-kefir.js: Select application again in Kefir.js
for (var i = 0; i < shapes.length; i++) if (insidePolygon(pos, shapes[i])) return shapes[i].id;
return null;
}).toProperty("cat");
var okButton = document.getElementById('ok');
var sOK = Kefir.fromBinder(function(emitter) { okButton.addEventListener("click", emitter.emit) return function() {
okButton.removeEventListener("click", emitter.emit);
} });
selected.sampledBy(sOK, function(sel, ok) { return sel; }) .onValue(function(sel) {
alert('You selected '+sel);
});
selected.onValue(function(selected) { ...
To run this example, point your browser at sodium/book/web/select-kefir.html.
6.3.3 And now…Flapjax
Here’s the same example in Flapjax (see listing 6.7). Flapjax is based more on “classic”
FRP, so it’s a little closer to Sodium than to RxJS.
Flapjax uses the old-school names Event (for stream) and Behavior (for cell);
hence the E and B suffixes of its primitives. mapE corresponds to Sodium’s map on streams, and liftB corresponds to Sodium’s map on cells.
These two double as listen functionality for feeding outputs to I/O handling. We’re not so keen on this idea because we think it’s better to keep these concepts separate in people’s minds: “This is mapE, and it’s for referentially transparent logic. This is listen, and it’s for I/O.”
startsWith corresponds to Sodium’s hold—not to RxJS’s startWith(). And snapshotE is exactly snapshot.
NOTE We maintain that FRP is FRP is FRP—that despite some surface differ- ences, all FRP systems are substantially based on the same small set of simple concepts.
function init() {
var canvas = document.getElementById("myCanvas");
var getXY = function(e) { return { x : e.pageX - canvas.offsetLeft, y : e.pageY - canvas.offsetTop }; };
var mouseDown = extractEventE(canvas,'mousedown').mapE(getXY);
var selected = mouseDown.mapE(function(pos) { for (var i = 0; i < shapes.length; i++) if (insidePolygon(pos, shapes[i])) return shapes[i].id;
return null;
Listing 6.7 select-flapjax.js: Select application again this time in Flapjax
121 The latest of two observables with combineLatest
}).startsWith("cat");
var okButton = document.getElementById('ok');
var ok = clicksE(okButton);
snapshotE(ok, selected, function(ok, sel) { return sel; }) .mapE(function(sel) {
alert('You selected '+sel);
});
...
To run this example, point your browser at sodium/book/web/select-flapjax.html.