Skip to content

Instantly share code, notes, and snippets.

@whiteinge
Last active May 15, 2024 20:11
Show Gist options
  • Save whiteinge/67c6805d2c508611068e4c11d1d16154 to your computer and use it in GitHub Desktop.
Save whiteinge/67c6805d2c508611068e4c11d1d16154 to your computer and use it in GitHub Desktop.
A bare-bones minimal (and probably slightly incorrect) implementation of Maybe, Either, IO, and Task
class Box {
constructor(x) { this._x = x }
map(f) { return new Box(f(this._x)) }
}
class LazyBox {
constructor(g) { this.g = g }
map(f) { return new LazyBox(() => f(this.g())) }
}
// ----------------------------------------------------------------------------
// Multi-purpose Maybe, Either, XhrResult. (Don't judge me!)
class ME {
static Left(x) { return new ME(x, 'Left') }
static Right(x) { return new ME(x, 'Right') }
static Loading(x) { return new ME(x, 'Loading') }
static Initial(x) { return new ME(x, 'Initial') }
static Nothing() { return ME.Left() }
static Error(x) { return ME.Left(x) }
static Ok(x) { return ME.Right(x) }
static of(x) { return ME.Right(x) }
static fromNullable(x) { return x != null ? ME.Right(x) : ME.Left(x) }
static tryCatch(f, ...args) {
try { return new ME.Right(f(...args)) }
catch(e) { return new ME.Left(e) }
}
constructor(val, type) { this.val = val; this.type = type }
map(f) { return this.type === 'Right' ? ME.Right(f(this.val)) : this }
chain(f) { return this.type === 'Right' ? f(this.val) : this }
fold(f, g, h, i) {
switch(this.type) {
case 'Left': return f(this.val);
case 'Right': return g(this.val);
case 'Loading': return h(this.val);
case 'Initial': return i(this.val);
}
}
getOrElse(def) { return this.type === 'Right' ? this.val : def }
}
/**
Maybe.of('foo').map(x => x.toUpperCase()).getOrElse('doh')
Maybe.Nothing().map(x => x).getOrElse('doh')
**/
class Maybe extends ME {}
/**
Either.of('foo').map(x => x.toUpperCase()).fold(console.error, console.log)
Either.Left('foo').map(x => x).fold(console.error, console.log)
**/
class Either extends ME {}
/**
XhrResult.Ok('foo')
.chain(() => XhrResult.Ok('bar'))
.fold(
x => console.log('err', x),
x => console.log('ok', x),
x => console.log('load', x),
x => console.log('init', x));
**/
class XhrResult extends ME {}
// ----------------------------------------------------------------------------
/**
Task.of((reject, resolve) => setTimeout(resolve, 1000, 5))
.map(x => x + 1)
.fork(console.error, console.log);
**/
class Task {
constructor(fork) { this.fork = fork }
map(f) { return new Task((reject, resolve) => this.fork(reject, x => resolve(f(x)))) }
chain(f) { return new Task((reject, resolve) => this.fork(reject, x => f(x).fork(reject, resolve))) }
join() { return new Task(this.fork).chain(i => i) }
ap(A) { return A.map(a => new Task(this.fork).map(f => f(a))).join() }
static fromPromise(f, ...args) { return new Task((reject, resolve) => f(...args).then(resolve, reject)) }
static of(fork) { return new Task(fork) }
}
// ----------------------------------------------------------------------------
/**
IO.of(x => window.location.href).map(x => x.toUpperCase()).run()
**/
class IO {
constructor(fn) { this.run = fn; }
map(fn) { return new IO(() => fn(this.run())) }
chain(fn) { return new IO(() => fn(this.run()).run()) }
static of(fn) { return new IO(fn) }
}
// ----------------------------------------------------------------------------
/**
Collection sum type monoid for adding and removing values from an object
Collection.Add({foo: 'Foo'}).concat(Collection.Del({foo: 'Foo'}));
**/
class Collection {
static of(x) { return Collection.Add(x) }
static empty() { return Collection.Add({}) }
static Add(x) { return new Collection(x, 'Add') }
static Del(x) { return new Collection(x, 'Del') }
constructor(val, type) { this.val = val; this.type = type }
concat(x) {
if (x.type === 'Add') { Object.assign(this.val, x.val); return this }
if (x.type === 'Del') {
Object.keys(x.val).forEach(key => delete this.val[key]);
return this;
}
}
}
// ----------------------------------------------------------------------------
/**
const pageTitle = new View(({ title }) => h('h1', title));
const paragraph = new View(({ text }) => h('p', text));
pageTitle
.contramap(x => ({...x, title: x.title.toUpperCase()}))
.concat(paragraph
.map(x => h('div', x))
.contramap(x => ({...x, text: `${x.text}!!`})))
.fold({title: 'Hello, there', text: 'Some text here'});
**/
class View {
constructor(fold) { this.fold = fold }
map(f) { return new View(props => f(this.fold(props))) }
contramap(g) { return new View(props => this.fold(g(props))) }
concat(other) {
var that = this;
return new View(function(props) {
const frag = document.createDocumentFragment();
frag.appendChild(that.fold(props));
frag.appendChild(other.fold(props));
return frag;
});
}
static of(x) { return new View(() => x) }
static empty() { return View.of(document.createDocumentFragment()) }
}
// ----------------------------------------------------------------------------
// Stolen from Lodash:
const htmlEscapes = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'};
const reUnescapedHtml = /[&<>"']/g;
const reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
function escape(string) {
return string && reHasUnescapedHtml.test(string)
? string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr])
: string || '';
}
// Stolen from 1-liners:
const zip = (...arrays) => Array
.from({ length: Math.max(...arrays.map(a => a.length)) })
.map((_, i) => arrays.map(a => a[i]));
/**
const name = 'world';
const el = html`
<div>
<p>Hello, ${name}!</p>
</div>
`;
**/
function html(strings, ...vars) {
const escapedVars = vars.map(escape);
const content = zip(strings, escapedVars).flatMap(x => x.join('')).join('');
return document.createRange().createContextualFragment(content);
}
// ----------------------------------------------------------------------------
/**
h('div')
h('div', {className: 'foo'})
h('div', 'Foo')
h('div', ['Foo', ' ', 'Bar'])
h('div', [h('span', 'Foo'), h('span', 'Bar')])
h('div', h('span', 'Foo'))
h('div', {style: 'color: green'}, 'Foo')
**/
function hyperscript(isSVG, tag, a, b) {
const el = isSVG
? document.createElementNS('http://www.w3.org/2000/svg', tag)
: document.createElement(tag);
var attrs, children;
if (b !== undefined) {
attrs = a, children = b;
} else if (Object.prototype.toString.call(a) === '[object Object]') {
attrs = a, children = [];
} else {
attrs = {}, children = a;
}
Object.entries(attrs ?? {}).forEach(([key, val]) => {
if (key === 'style' && val.constructor === Object) {
Object.entries(val).forEach(([k, v]) => el.style.setProperty(k, v));
} else {
el[key] = val;
}
});
[].concat(children).forEach(child => {
if (child == null) return;
const childVal = (
child instanceof Element
|| child instanceof HTMLDocument
|| child instanceof DocumentFragment)
? child
: document.createTextNode(String(child));
el.appendChild(childVal);
});
return el;
}
const h = hyperscript.bind(null, false);
const s = hyperscript.bind(null, true);
// ----------------------------------------------------------------------------
class Observable {
constructor(_sub) { this._sub = _sub }
subscribe(next) { this._sub(next) }
map(f) { return new Observable(next =>
this.subscribe(x => next(f(x)))) }
do(f) { return new Observable(next =>
this.subscribe(x => { f(x); next(x) }) )}
scan(f, seed) { return new Observable(next => {
var acc = seed;
return this.subscribe(cur => { acc = f(acc, cur); next(acc) });
}) }
foldp(M, seed = M.empty()) {
return this.scan((acc, x) => acc.concat(x), seed).startWith(seed);
}
filter(f) { return new Observable(next =>
this.subscribe(x => f(x) && next(x))) }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment