Last active
August 26, 2024 01:15
-
-
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
This file contains 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
// 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