// Let's make it possible to create pure functions even when we're // dealing with impure operations that would have side effects! // First we'll need a "Type" that can contain a (sometimes impure) function function IO(fn) { if (!(this instanceof IO)) {//make it simpler for end users to create a type without "new" return new IO(fn); } this.runIO = fn;//IO now provides an extra control layer that allows the composition of unexecuted effects } // we'll need a way to get regular values "into" the IO type // we make them into "constant" functions: thunks that return the value when called IO.of = IO.prototype.of = x => new IO(_=>x); // we'll need a way to compose together the inner functions inside the IO context IO.prototype.map = function(f) { return this.chain( a => IO.of(f(a)) ); }; // to combine effects, we'll need a way to extract the "functional value" in an IO and use it to to make a new IO IO.prototype.chain = function(f) { return new IO(_ => f(this.runIO()).runIO() ); }; // we'll need a way to apply a value to an IO if its inner function returns a function IO.prototype.ap = function(a) { return this.chain( f => a.map(f)); }; // just for fun, we can schedule any operation to happen on the next frame // ( it will still be "inside" an IO, but will add an extra Promise layer inside when run) IO.prototype.fork = function(f) { return IO(_ => new Promise( r => window.setTimeout(()=>r(this.runIO()),0) )); }; // and now, the real magic: a helper to create an IO that will get dom elements via any selector string... IO.$ = selectorString => IO(_=>Array.from(document.querySelectorAll(selectorString))); // because DOM nodes are lists of things, let's also make it properly easy to work with lists, since the language doesn't // Yeah, we're modifying the native prototype: want to fight about it? Array.prototype.flatten = function(){return [].concat(...this); }; // our Array.flatMap. // Note that, to avoid silly results, we needed to guard the f against the extra args that native Array.map passes Array.prototype.chain = function(f){ return this.map(x=>f(x)).flatten(); }; // arrays of functions are cool, and we should also have a way to apply arrays of values to them Array.prototype.ap = function(a) { return this.reduce( (acc,f) => acc.concat( a.map(f) ), []); }; // we should also have a way to flip around an Array of types into a type of an Array Array.prototype.sequence = function(point){ return this.reduceRight( function(acc, x) { return acc .map(innerarray => othertype => [othertype].concat(innerarray) )//puts this function in the type .ap(x);//then applies the inner othertype value to it }, point([]) ); }; // since it's so common/useful, a combined way to map over elements in an Array and THEN flip it inside out Array.prototype.traverse = function(f, point){ return this.map(f).sequence(point); }; // heck, let's make it easier to deal with Promises too Promise.of = Promise.prototype.of = x => Promise.resolve(x) Promise.prototype.map = Promise.prototype.chain = Promise.prototype.then; // For Promises containing functions... Promise.prototype.ap = function(p2){ return Promise.all([this, p2]).then(([fn, x]) => fn(x)); } // alternate to the 2-argument .then // Should help avoid the common confusion over how .then(fn1,errorfn) won't catch an error thrown in fn1 Promise.prototype.bimap = function(e,s){//note that e is specified first, which FORCES us to deal with it return this.then(s).catch(e); }; // we'll want some helper functions probably, because common DOM methods don't exactly work like Arrays. Nice example: const getNodeChildren = node => Array.from(node.children); const setHTML = stringHTML => node => IO(_=> Object.assign(node,{innerHTML:stringHTML})); //Examples // Here's a pure description of an operation that would set the first child of the body element to be "boo!" const doBoo = IO.$('body')//always returns an Array .map(xs=>xs[0])//when we're altering the "value" inside an IO, we just map (Array of Nodes -> node) .map(getNodeChildren)//now we have an Array again so... .map(xs=>xs[0])//now we have a single node .chain(setHTML("boo!"));//when we're using another IO-returning operation, we need to flatMap // you can run that operation over and over, and no side-effects will happen. // The result will always be the same: an IO describing a particular sequence of effects with a runIO method // to actually run the effect, we'd need to explicitly call .runIO() on it // here's how you might wire up that effect to a user clicking //document.addEventListener('click', doBoo); // Now let's look at dealing with multiple DOM nodes at once // this IO will "boo!" ALL of the children of the children of the body const booAll = IO.$('body') .map(xs => xs[0])//aka: _.head .map(getNodeChildren)//node -> Array of Nodes .map(xs => xs.chain(getNodeChildren))//flatMaping gets us a SINGLE Array of nodes .chain( xs => xs.map(setHTML('boo!')).sequence(IO.of) );//we map an IO over every node, then flip it so it returns a single IO // here's a pointfree version of the same computation, if we have generic PF compose/chain/map/sequence/head/etc. // const doBoo2 = compose( // chain(compose(sequence(IO.of), map(setHTML("boo!")))), // map(chain(getNodeChildren)), // map(getNodeChildren), // map(head), // IO.$ // )('body'); // which simplifies to // const doBoo2 = compose( // chain( compose( sequence(IO.of), map(setHTML("boo!")) ) ), // map( compose( chain(getNodeChildren), getNodeChildren, head) ), // IO.$ // )('body'); // we always don't have to start with IO, even though we probably should const setChildHTMLtoHi = getNodeChildren(document.body)//-> [node] .chain(getNodeChildren)//-> [...nodes] .map(setHTML('h'))//-> [IO(nodeAction), IO(nodeAction), ...] .sequence(IO.of)//-> IO of all the node actions, like a Promise.all for an Array of IO actions //setChildHTMLtoHi.runIO();//-> runs the effect