Last active
December 12, 2025 23:10
-
-
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 hidden or 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()) } | |
| } | |
| // ---------------------------------------------------------------------------- | |
| const htmlEscapes = { | |
| '&': '&', | |
| '<': '<', | |
| '>': '>', | |
| '"': '"', | |
| "'": ''', | |
| }; | |
| 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