Skip to content

Instantly share code, notes, and snippets.

@whiteinge
Last active December 12, 2025 23:10
Show Gist options
  • Select an option

  • Save whiteinge/67c6805d2c508611068e4c11d1d16154 to your computer and use it in GitHub Desktop.

Select an option

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()) }
}
// ----------------------------------------------------------------------------
const htmlEscapes = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
const reUnescapedHtml = new RegExp(`[${Object.keys(htmlEscapes).join()}]`, 'g');
export const escape = (raw) => {
// Always coerce to string so objects with toString() can't bypass.
const string = String(raw ?? '');
return string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr]);
};
/**
Create HTML DOM elements using a tagged template literal
const name = 'world';
const el = html`
<div>
<p>Hello, ${name}!</p>
</div>
`;
Note, this is only useful for safely escaping variables containing user input.
Toggling boolean attributes or adding event handlers should be done by
post-processing the returned DOM elements.
**/
export const html = (strings, ...vars) => {
let [head, ...tail] = strings;
const content = head.concat(
vars
.flatMap((v, idx) => [escape(v), tail[idx]])
.join(''),
);
return window.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')
h('div', null, 'Foo')
h('div', {onclick: () => alert('Clicked')}, 'Foo')
h('input', {disabled: true}, 'Foo')
h('input', {disabled: false}, 'Foo')
**/
const hyperscript = (isSVG, tag, ...args) => {
const el = isSVG
? document.createElementNS('http://www.w3.org/2000/svg', tag)
: document.createElement(tag);
const attrs =
typeof args[0] === 'object' && args[0] !== null && !Array.isArray(args[0])
? args.shift()
: {};
Object.entries(attrs).forEach(([key, val]) => {
if (val == null || val === false) return;
else if (key === 'style' && typeof val === 'object') {
return Object.entries(val).forEach(([k, v]) =>
el.style.setProperty(k, v)
);
}
else if (key.startsWith('on') && typeof val === 'function') {
return el.addEventListener(key.slice(2).toLowerCase(), val);
}
el.setAttribute(key, val === true ? '' : val);
});
args.flat().forEach((child) => {
if (child == null) return;
el.appendChild(
child instanceof Node
? child
// Coerce anything else to a string.
: document.createTextNode(String(child)),
);
});
return el;
};
export const h = hyperscript.bind(null, false);
export 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