Skip to content

Instantly share code, notes, and snippets.

@SeijiEmery
Last active June 3, 2017 01:58
Show Gist options
  • Save SeijiEmery/beb90c839dea40c25fbbc00d96cc4555 to your computer and use it in GitHub Desktop.
Save SeijiEmery/beb90c839dea40c25fbbc00d96cc4555 to your computer and use it in GitHub Desktop.
FRP example (JS)
//
// This is a super-crappy, half-finished FRP / reactive library I wrote in JS to work
// through some FRP (Rx) and event dispatching concepts.
//
// This is not real FRP (it's a crappy infinite / unending event stream with some really
// basic higher-order functions), and it's event only; no observables, behavior, push / pull
// (this is push only), etc.
//
// But it was sorta useful as a bit of code to work through some stuff, and it's enough
// code to show really basic input filtering and how to listen and inject events (with
// very low level code). It's undoutedbly inefficient, but it's < 80 lines of JS, so
// that's neat. JS is also a way better language to model this in (initially) than, say, c++.
//
function EventStream () {
this.subscribers = new Array(); // list of things (other event streams) we'll dispatch to
this.alive = true; // marker so we can delete event listeners / streams on the fly
this.id = EventStream.nextId++; // unique id for debugging purposes
}
EventStream.nextId = 0; // this and toString() just exists for debugging
EventStream.prototype.toString = function () {
return "[EventStream "+this.id+"]";
}
// Adds a child event stream that we'll delegate our events to (push only / one directional).
EventStream.prototype.subscribe = function subscribe (subscriber) {
console.log("Subscribing "+subscriber+" <= "+this);
this.subscribers.push(subscriber);
}
// Fire an event, calling dispatch() on all attached subscribers
// Additionally, has a bit of logic to detach any subscribers where alive == false,
// so you can set this and objects will detach themselves when dispatch() is called.
// Note: the subscriber list is NOT ordered, so the order that events trickle through
// the graph is completely arbitrary and can / will change if anything gets removed.
EventStream.prototype.dispatch = function dispatch (event) {
for (var l = this.subscribers.length, i = l; i --> 0; ) {
if (!this.subscribers[i].alive) {
console.log("subscribers["+i+"] = "+this.subscribers[i]+" died, removing");
this.subscribers[i] = this.subscribers[--l];
this.subscribers.pop();
} else {
this.subscribers[i].dispatch(event);
}
}
}
// Tiny object that inherits from EventStream; overrides the dispatch method on a per-object
// basis so we can do more interesting things. We can still call the original method via
// EventStream.prototype.dispatch.call(this, ...)
function Subscriber (parent, dispatchFcn) {
EventStream.call(this);
this.dispatch = dispatchFcn;
parent.subscribe(this);
}
Subscriber.prototype = Object.create(EventStream.prototype);
Subscriber.prototype.constructor = Subscriber;
// Simplest kind of subscriber: takes a function and applies that to every event we recieve.
EventStream.prototype.apply = function apply (fcn) {
return new Subscriber(this, fcn);
}
// Returns a new stream with the inputs mapped through fcn.
EventStream.prototype.map = function map (fcn) {
return new Subscriber(this, function(event) {
EventStream.prototype.dispatch.call(this, fcn(event));
});
}
// Returns a new stream with the inputs filtered by fcn.
EventStream.prototype.filter = function filter (fcn) {
return new Subscriber(this, function(event) {
if (fcn(event)) {
EventStream.prototype.dispatch.call(this, event);
}
});
}
// Merges two event streams (returns a new stream).
EventStream.prototype.merge = function merge (other) {
var subscriber = new EventStream();
this.subscribe(subscriber);
other.subscribe(subscriber);
return subscriber;
}
// Scans through input with a persistent variable; this is basically fold / reduce,
// except it produces a value with every call.
EventStream.prototype.scan = function scan (initial, fcn) {
var value = initial;
return new Subscriber(this, function (event) {
EventStream.prototype.dispatch.call(this, value = fcn(value, event));
});
}
// And this is sorta redundant, but whatever.
function notNull (value) { return value !== undefined && value !== null; }
EventStream.prototype.filterNotNull = function () {
return EventStream.prototype.filter.call(this, notNull);
}
//
// Usecase example: simple mouse events
//
// Define our event types (mouse click events, double click events produced by mouse click events).
function MouseEvent () { this.time = new Date().getTime() * 1e-3; }
MouseEvent.prototype.toString = function () { return "MouseEvent"; }
function MouseClickEvent (x, y, button, state) {
MouseEvent.apply(this);
this.name = "click";
this.x = x || 0.0;
this.y = y || 0.0;
this.button = button || "lmb";
this.state = state || "down";
}
MouseClickEvent.prototype = Object.create(MouseEvent.prototype);
MouseClickEvent.prototype.constructor = MouseClickEvent;
MouseClickEvent.prototype.toString = function () { return "MouseClickEvent"; }
function DoubleClickEvent (button, count) {
MouseEvent.apply(this);
this.name = "doubleclick";
this.button = button;
this.count = count;
}
DoubleClickEvent.prototype = Object.create(MouseEvent.prototype);
DoubleClickEvent.prototype.constructor = MouseClickEvent;
DoubleClickEvent.prototype.toString = function () { return "DoubleClickEvent"; }
var allEvents = new EventStream();
var mouseClickEvents = allEvents.filter(function (event) { return event instanceof MouseClickEvent; });
var doubleClickEvents = (function () {
// Bit of state to track button press count and last press time for each button
// This is in a closure to hide access to this variable.
const doubleClickInfo = {
lmb: { count: 0, time: 0 },
rmb: { count: 0, time: 0 },
mmb: { count: 0, time: 0 },
maxDelay: 0.25,
};
// We produce new events, by mapping, returning non-null when we have an event we
// actually want to inject, and then filtering non-null values.
// I was initially going to use scan, but realized that a reduce-type function wasn't
// gonna really work since we need to be returning new events *and* preserving state
// (hence map + external mutable state; yes, this is evil).
return mouseClickEvents
.map(function (event) {
// console.log("Got event: "+event);
if (!event.state || !event.button || event.state != "down")
return null;
var btn = doubleClickInfo[event.button];
if (!btn) {
console.log("No button "+event.button+"!")
return null;
}
if (event.time - btn.time > doubleClickInfo.maxDelay) {
btn.count = 0;
}
btn.time = event.time;
if (++btn.count >= 2) {
return new DoubleClickEvent(event.button, btn.count);
}
return null;
})
.filterNotNull();
})();
// Finally, we merge our double click and regular events back in with merge().
// And then we just listen to this, filtered by whatever event type we want to respond to.
// And yeah, instanceof is a shitty way to check event types.
var mouseEvents = mouseClickEvents.merge(doubleClickEvents);
mouseEvents
.filter(function (event) { return event instanceof MouseClickEvent; })
.apply(function (event) { console.log("Mouse clicked: "+event.x+", "+event.y+", button = "+event.button); })
mouseEvents
.filter(function (event) { return event instanceof DoubleClickEvent; })
.apply(function (event) { console.log("Double click: "+event.button+", count = "+event.count); })
// Now that this is wired up, we fire our regular mouse events through allEvents,
// which works its way through the event graph and creates new double click events.
// Everything works, so yay.
allEvents.dispatch(new MouseClickEvent(0, 1, "lmb", "down"));
allEvents.dispatch(new MouseClickEvent(1, 2, "rmb", "up"));
allEvents.dispatch(new MouseClickEvent(33, 49, "lmb", "up"));
allEvents.dispatch(new MouseClickEvent(0, 1, "lmb", "down"));
allEvents.dispatch(new MouseClickEvent(0, 1, "lmb", "down"));
allEvents.dispatch(new MouseClickEvent(0, 1, "lmb", "down"));
allEvents.dispatch(new MouseClickEvent(0, 1, "lmb", "down"));
Subscribing [EventStream 1] <= [EventStream 0]
Subscribing [EventStream 2] <= [EventStream 1]
Subscribing [EventStream 3] <= [EventStream 2]
Subscribing [EventStream 4] <= [EventStream 1]
Subscribing [EventStream 4] <= [EventStream 3]
Subscribing [EventStream 5] <= [EventStream 4]
Subscribing [EventStream 6] <= [EventStream 5]
Subscribing [EventStream 7] <= [EventStream 4]
Subscribing [EventStream 8] <= [EventStream 7]
Mouse clicked: 0, 1, button = lmb
Mouse clicked: 1, 2, button = rmb
Mouse clicked: 33, 49, button = lmb
Mouse clicked: 0, 1, button = lmb
Double click: lmb, count = 2
Mouse clicked: 0, 1, button = lmb
Double click: lmb, count = 3
Mouse clicked: 0, 1, button = lmb
Double click: lmb, count = 4
Mouse clicked: 0, 1, button = lmb
Double click: lmb, count = 5
[Finished in 0.1s]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment