Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Bare bones FP type utility lib so we can play around with functions that capture the composition of DOM read/writes, but in a pure way
// 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.