Last active
May 15, 2024 20:11
-
-
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
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
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 = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}; | |
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