Last active
June 3, 2017 01:58
-
-
Save SeijiEmery/beb90c839dea40c25fbbc00d96cc4555 to your computer and use it in GitHub Desktop.
FRP example (JS)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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")); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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