Skip to content

Instantly share code, notes, and snippets.

@dtipson
Last active August 26, 2024 01:15
Show Gist options
  • Save dtipson/14c033229d6ebc94bc612776423150e4 to your computer and use it in GitHub Desktop.
Save dtipson/14c033229d6ebc94bc612776423150e4 to your computer and use it in GitHub Desktop.
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