From 699a8e6a7c4afbdac37d6016ee846b3e2da53be4 Mon Sep 17 00:00:00 2001 From: rowan Date: Sat, 5 Apr 2025 22:39:39 -0500 Subject: [PATCH] :3c --- jsconfig.json | 2 +- package-lock.json | 16 ++ package.json | 10 +- src/algebra/common.js | 56 ------- src/algebra/fn.js | 45 ++++++ src/algebra/free.js | 140 ++++++++++++++++++ src/algebra/index.js | 331 +++++++++++++++++++++++++++++++++++++++++- src/algebra/list.js | 54 +++++++ src/algebra/option.js | 207 +++++++++++++------------- src/algebra/result.js | 258 +++++++++++++++++--------------- src/algebra/types.js | 33 +++++ src/mixin.js | 256 ++++++++++++++++++++++++++++++++ src/union.js | 14 +- 13 files changed, 1138 insertions(+), 284 deletions(-) delete mode 100644 src/algebra/common.js create mode 100644 src/algebra/fn.js create mode 100644 src/algebra/free.js create mode 100644 src/algebra/list.js create mode 100644 src/algebra/types.js create mode 100644 src/mixin.js diff --git a/jsconfig.json b/jsconfig.json index c50e3ed..6ec80e6 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "module": "es2020", "target": "es6", - "lib": ["es2019", "dom"], + "lib": ["es2022", "dom"], "checkJs": true, "paths": { "/*": ["./*"] diff --git a/package-lock.json b/package-lock.json index 230b43d..9369ea7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "kojima", "version": "1.0.0", "license": "GPL-3.0-or-later", + "dependencies": { + "typescript": "^5.8.2" + }, "devDependencies": { "folktest": "git+https://git.kitsu.cafe/rowan/folktest.git" } @@ -17,6 +20,19 @@ "resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#b130e6fd1839a32ca62ffe9c96da58d8bdf39b38", "dev": true, "license": "GPL-3.0-or-later" + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } } } } diff --git a/package.json b/package.json index 2903b93..8c1e88b 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,17 @@ "scripts": { "test": "./tests/index.js" }, - "keywords": ["functional", "functional programming", "fp", "monad"], + "keywords": [ + "functional", + "functional programming", + "fp", + "monad" + ], "license": "GPL-3.0-or-later", "devDependencies": { "folktest": "git+https://git.kitsu.cafe/rowan/folktest.git" + }, + "dependencies": { + "typescript": "^5.8.2" } } diff --git a/src/algebra/common.js b/src/algebra/common.js deleted file mode 100644 index 890458d..0000000 --- a/src/algebra/common.js +++ /dev/null @@ -1,56 +0,0 @@ -export const Self = Symbol() -export const Value = Symbol() - -export function constant() { return this } - -/** - * @template T, U - * @typedef {(x: T) => U} Morphism - */ - -/** - * @template T - * @typedef {{ [Value]: T }} Set - */ - -/** - * @template T - * @this {Construct} - * @typedef {{ [Self]: (value: a) => Construct }} Construct - */ - -/** - * @template T - * @param {T} value - * @returns {Set} - */ -export const Set = value => ({ [Value]: value }) - -/** - * @template T, U - * @param {Morphism} fn - * @returns {U} - */ -export function chain(fn) { - return fn(this[Value]) -} - -/** - * @template T - * @this {Set} - * @returns {T} - */ -export function extract() { - return this[Value] -} - -/** - * @template T, U - * @this {Construct} - * @param {U} value - * @returns {Construct} - */ -export function of(value) { - return this[Self](value) -} - diff --git a/src/algebra/fn.js b/src/algebra/fn.js new file mode 100644 index 0000000..ec38801 --- /dev/null +++ b/src/algebra/fn.js @@ -0,0 +1,45 @@ +import { curry } from '../curry.js' +import { Free, Suspend, pure } from './free.js' + +/** @import {InferredMorphism, Morphism, ChainConstructor} from './types.js' */ + +/** + * @template T + * @param {T} x + * @returns {x} + */ +export const id = x => x + +export const compose = curry( + /** + * @template T, U, V + * @param {Morphism} f + * @param {Morphism} g + * @param {T} x + * @returns V + */ + (f, g, x) => f(g(x)) +) + +export const kleisliCompose = curry( + /** + * @template {ChainConstructor} M, T, U + * @param {M} f + * @param {Morphism} g + * @param {T} x + * @returns U + */ + (f, g, x) => f(x).chain(g) +) + +/** + * @template T + * @param {T} x + * @returns {Free} + */ +export const liftF = x => new Suspend( + x, + /** @type {InferredMorphism} */(pure) +) + + diff --git a/src/algebra/free.js b/src/algebra/free.js new file mode 100644 index 0000000..80b75fb --- /dev/null +++ b/src/algebra/free.js @@ -0,0 +1,140 @@ +import { liftF } from './fn.js' +import { Algebra, Comonad, EitherOf, Functor, Monad } from './index.js' + +/** @import { InferredMorphism, Morphism } from './types.js' */ + +/** @template T */ +export class Suspend extends Algebra(Functor, Monad) { + #value + #fn + + /** + * @param {T} value + * @param {InferredMorphism} fn + */ + constructor(value, fn) { + super() + this.#value = value + this.#fn = fn + } + + /** @returns {this is Pure} */ + isPure() { return false } + + /** @returns {this is Suspend} */ + isSuspend() { return true } + + /** + * @template U + * @param {Morphism>} f + * @returns {Suspend} + */ + chain(f) { + return new Suspend(this.#value, x => this.#fn(x).chain(f)) + } + + /** + * @template U + * @param {Morphism} g + * @returns {Suspend} + */ + map(g) { + return new Suspend(this.#value, x => this.#fn(x).map(g)) + } + + /** + * @template U + * @param {Morphism} g + * @returns {Suspend} + */ + then(g) { + return this.map(g) + } + + run() { + return this.#fn(this.#value) + } +} + +/** @template T */ +export class Pure extends Algebra(Functor, Monad, Comonad) { + #value + + /** @param {T} value */ + constructor(value) { + super() + this.#value = value + } + + /** @returns {this is Pure} */ + isPure() { return true } + + /** @returns {this is Suspend} */ + isSuspend() { return false } + + /** + * @template U + * @param {Morphism>} f + * @returns {Pure} + */ + chain(f) { + return f(this.#value) + } + + /** + * @template U + * @param {Morphism} f + * @returns {Pure} + */ + map(f) { + return this.chain(x => new Pure(f(x))) + } + + /** + * @template R + * @param {Morphism} f + * @returns {Pure} + */ + then(f) { + return this.map(f) + } + + extract() { + return this.#value + } + + run() { + return this.#value + } +} + +/** + * @template T + * @type {Pure | Suspend} Free + */ +export class Free { + /** + * @template T + * @param {T} value + * @returns {Free} + */ + static of(value) { + return liftF(value) + } +} + +/** + * @template T + * @param {T} value + * @returns {Pure} + */ +export const pure = value => new Pure(value) + +/** + * @template T + * @param {T} value + * @param {InferredMorphism} f + * @returns {Suspend} + */ +export const suspend = (value, f) => new Suspend(value, f) + diff --git a/src/algebra/index.js b/src/algebra/index.js index 9d71a84..884d3f5 100644 --- a/src/algebra/index.js +++ b/src/algebra/index.js @@ -1,3 +1,330 @@ -export { Some, None, Option } from './option.js' -export { Ok, Err, Result } from './result.js' +import { mix } from '../mixin.js' + +/** @import { MixinFunction } from '../mixin.js' */ + +export const ProtectedConstructor = Symbol('ProtectedConstructor') +export class ProtectedConstructorError extends Error { + /** @param {string} name */ + constructor(name) { + super(`ProtectedConstructorError: ${name} cannot be directly constructed`) + } +} + +class Derivations { + #inner + + /** + * @param {Iterable} iterable + */ + constructor(iterable) { + this.#inner = new Map(iterable) + } + + /** + * @param {Iterable} keys + * @param {any} value + * @returns {this} + */ + set(keys, value) { + this.#inner.set(new Set(keys), value) + return this + } + + /** + * @param {Iterable} keys + * @returns {boolean} + */ + has(keys) { + return this.#inner.has(new Set(keys)) + } + + /** + * @param {Iterable} keys + */ + get(keys) { + return this.#inner.get(new Set(keys)) + } + + /** + * @param {Iterable} keys + */ + delete(keys) { + return this.#inner.delete(new Set(keys)) + } +} + +export class NotImplementedError extends Error { + /** @param {string} name */ + constructor(name) { + super(`${name} is not implemented`) + } +} + +/** @typedef {(mixin: MixinFunction) => MixinFunction} MixinWrapper */ + +/** + * @enum {number} + */ +const MethodType = Object.freeze({ + Instance: 0, + Static: 1 +}) + +class Method { + #name + + /** @type {MethodType} */ + #type = MethodType.Instance + + /** + * @param {string | Method} name + */ + constructor(name) { + if (name instanceof Method) { + return name + } + + this.#name = name + } + + /** + * @param {string | Method} value + */ + static from(value) { + return new Method(value) + } + + isInstance() { + this.#type = MethodType.Instance + return this + } + + isStatic() { + this.#type = MethodType.Static + return this + } + + /** + * @param {Function} target + */ + _getInstallationPoint(target) { + switch (this.#type) { + case 0: return target.prototype + case 1: return target + default: return target + } + } + + /** + * @param {Interface} builder + * @param {Function} target + */ + implement(builder, target) { + const err = new NotImplementedError( + `${builder.name}::${this.#name}` + ) + + this._getInstallationPoint(target)[this.#name] = function() { + throw err + } + } +} + +class Interface { + #name + + /** @type {Set} */ + #methods = new Set() + + /** @type {Set} */ + #interfaces = new Set() + + /** @param {string} name */ + constructor(name) { + this.#name = name + } + + get name() { + return this.#name + } + + /** + * @param {...(PropertyKey | Method)} methods + * @returns {this} + */ + specifies(...methods) { + this.#methods = new Set( + methods + .map(Method.from) + .concat(...this.#methods) + ) + + return this + } + + /** + * @param {...Interface} interfaces + * @returns {this} + */ + extends(...interfaces) { + this.#interfaces = new Set(interfaces.concat(...this.#interfaces)) + return this + } + + /** + * @returns {MixinWrapper} + */ + intoWrapper() { + return this.build.bind(this) + } + + /** + * @param {FunctionConstructor} base + */ + build(base) { + const interfaces = [...this.#interfaces.values()].map(x => x.intoWrapper()) + + const Interfaces = mix(base).with(...interfaces) + + const Algebra = class extends Interfaces { } + + for (const method of this.#methods) { + method.implement(this, Algebra) + } + + return Algebra + } +} + +class BaseSet { } + +/** + * @param {PropertyKey} key + * @param {object} obj + * @returns {boolean} + */ +export const hasOwn = (key, obj) => Object.hasOwn(obj, key) + +/** + * @param {Function} left + * @param {Function} right + */ +export const EitherOf = (left, right) => mix(left).with(l => right(l)) + +/** + * @param {string} methodName + * @param {(...args: any[]) => any} f + * @param {object} obj + */ +export const apply = (methodName, f, obj) => { + if (hasOwn(methodName, obj)) { + return obj[methodName](f) + } else { + return f(obj) + } +} + +/** + * @param {...Interface} algebras + * @returns {FunctionConstructor} + */ +export const Algebra = (...algebras) => { + return mix(BaseSet).with(...algebras.map(x => x.intoWrapper())) +} + +export const Setoid = new Interface('Setoid') + .specifies('equals') + +export const Ord = new Interface('Ord') + .specifies('lte') + +export const Semigroupoid = new Interface('Semigroupoid') + .specifies('compose') + +export const Category = new Interface('Category') + .extends(Semigroupoid) + .specifies( + Method.from('id').isStatic() + ) + +export const Semigroup = new Interface('Semigroup') + .specifies('concat') + +export const Monoid = new Interface('Monoid') + .extends(Semigroup) + .specifies( + Method.from('empty').isStatic() + ) + +export const Group = new Interface('Group') + .extends(Monoid) + .specifies('invert') + +export const Filterable = new Interface('Filterable') + .specifies('filter') + +export const Functor = new Interface('Functor') + .specifies('map') + +export const Contravariant = new Interface('Contravariant') + .specifies('contramap') + +export const Apply = new Interface('Apply') + .extends(Functor) + .specifies('ap') + +export const Applicative = new Interface('Applicative') + .extends(Apply) + .specifies( + Method.from('of').isStatic() + ) + +export const Alt = new Interface('Alt') + .extends(Functor) + .specifies('alt') + +export const Plus = new Interface('Plus') + .extends(Alt) + .specifies( + Method.from('zero').isStatic() + ) + +export const Alternative = new Interface('Alternative') + .extends(Applicative, Plus) + +export const Foldable = new Interface('Foldable') + .specifies('fold') + +export const Traversable = new Interface('Traversable') + .extends(Functor, Foldable) + .specifies('traverse') + +export const Chain = new Interface('Chain') + .extends(Apply) + .specifies('chain') + +export const ChainRef = new Interface('ChainRec') + .extends(Chain) + .specifies( + Method.from('chainRec').isStatic() + ) + +export const Monad = new Interface('Monad') + .extends(Applicative, Chain) + +export const Extend = new Interface('Extend') + .extends(Functor) + .specifies('extend') + +export const Comonad = new Interface('Comonad') + .extends(Extend) + .specifies('extract') + +export const Bifunctor = new Interface('Bifunctor') + .extends(Functor) + .specifies('bimap') + +export const Profunctor = new Interface('Profunctor') + .extends(Functor) + .specifies('promap') diff --git a/src/algebra/list.js b/src/algebra/list.js new file mode 100644 index 0000000..9246a17 --- /dev/null +++ b/src/algebra/list.js @@ -0,0 +1,54 @@ +import { liftF } from './fn.js' +import { Free } from './free.js' +import { Algebra, Foldable, Functor, Monad } from './index.js' +import { none as None } from './option.js' + +/** @template T */ +export class List extends Algebra(Functor, Monad, Foldable) { + /** @type {Free | None} */ + _head + + /** @type {List} */ + _tail + + /** + * @param {T | None} [head] + * @param {List} [tail=List] + */ + constructor(head = None, tail = List.of(None)) { + super() + this._head = head + this._tail = tail + } + + /** + * @template T + * @param {T} value + * @returns {List} + */ + prepend(value) { + return new List(value, this) + } + + /** + * @template T + * @param {T} value + * @returns {List} + */ + static of(value) { + return new List(value) + } + + /** + * @template T + * @param {Iterable} iterable + * @returns {List} + */ + static from(iterable) { + Array.from(iterable).reduceRight((list, value)) + } +} + +const list = liftF(1) +console.log(list.map(_ => 2).map(_ => 3).chain(x => x).run()) + diff --git a/src/algebra/option.js b/src/algebra/option.js index 1d32cf7..96158a5 100644 --- a/src/algebra/option.js +++ b/src/algebra/option.js @@ -1,122 +1,125 @@ -import { chain, constant, Self, Value } from './common.js' +import { Algebra, Foldable, Functor, Monad } from './index.js' -/** @import { Construct, Morphism, Set } from './common.js' */ +/** @import { Morphism } from './types.js' */ -/** - * @template T - * @typedef {Some | None} Option - */ +/** @template T */ +export class Some extends Algebra(Monad, Foldable) { + /** @type {T} */ + #value -/** - * @template T - * @typedef SomeMethods - * @property {typeof isSome} isSome - * @property {typeof isNone} isNone - * @property {typeof chain} chain - * @property {typeof map} map - * @property {typeof alt} alt - * @property {typeof fold} fold - */ + /** @param {T} value */ + constructor(value) { + super() -/** - * @template T - * @typedef {Construct & Set & SomeMethods} Some - * @variation 1 - */ + this.#value = value + } -/** - * @typedef None - * @property {typeof isSome} isSome - * @property {typeof isNone} isNone - * @property {typeof chain} chain - * @property {typeof map} map - * @property {typeof alt} alt - * @property {typeof fold} fold - */ + /** + * @template T + * @param {T} value + * @returns {Some} + */ + static of(value) { + return new Some(value) + } -/** - * @template T, U - * @this {Option} - * @param {Morphism} fn - * @returns {Option} - */ -function map(fn) { - return this[Self](this.chain(fn)) + /** + * @template R + * @param {Morphism} f + * @returns {R} + */ + chain(f) { + return f(this.#value) + } + + /** + * @template R + * @param {Morphism} f + * @returns {Some} + */ + map(f) { + return Some.of(this.chain(f)) + } + + /** + * @template R + * @param {Morphism} f + * @returns {Some} + */ + then(f) { + return this.map(f) + } + + /** + * @template U + * @param {(acc: U, value: T) => U} f + * @param {U} init + * @returns {U} + */ + reduce(f, init) { + return f(init, this.#value) + } } -/** - * @template T, U - * @this {Option} - * @param {Morphism} fn - * @param {U} acc - * @return {U} - */ -function fold(fn, acc) { - const result = this.map(fn) - return result.isSome() ? result[Value] : acc +/** @template T */ +class None extends Algebra(Monad, Foldable) { + /** + * @template R + * @param {Morphism} _f + * @returns {None} + */ + chain(_f) { + return this + } + + /** + * @template R + * @param {Morphism} _f + * @returns {None} + */ + map(_f) { + return this + } + + /** + * @template R + * @param {Morphism} _f + * @returns {None} + */ + then(_f) { + return this + } + + /** + * @template U + * @param {(acc: U, value: T) => U} _f + * @param {U} init + * @returns {U} + */ + reduce(_f, init) { + return init + } } /** * @template T - * @this {Option} - * @param {Option} other - * @returns {Option} + * @type {Some | None} Option */ -function alt(other) { - return this.isSome() ? this : other +export class Option { + /** + * @template T + * @param {T} value + */ + static of(value) { + return Some.of(value) + } } /** * @template T - * @this {Option} - * @returns {this is Some} + * @param {T} value */ -function isSome() { - return this[Self] === Some -} +export const some = value => new Some(value) -/** - * @template T - * @this {Option} - * @returns {this is None} - */ -function isNone() { - return this === None -} - -/** - * @template T - * @param {?T} value - * @returns {Some} - */ -export const Some = value => ({ - [Self]: Some, - [Value]: value, - isSome, - isNone, - chain, - map, - alt, - fold -}) - -/** - * @returns {None} - */ -export const None = () => None -None.isSome = isSome -None.isNone = isNone -None.chain = constant -None.map = constant -None.alt = alt -None.fold = fold - -/** - * @template T - * @param {?T} value - * @returns {Option} - */ -export const Option = value => Some(value) -Option.of = Option -Option.zero = None +export const none = new None() diff --git a/src/algebra/result.js b/src/algebra/result.js index 64f1448..833be79 100644 --- a/src/algebra/result.js +++ b/src/algebra/result.js @@ -1,147 +1,175 @@ -import { chain, constant, Self, Value } from './common.js' +import { Algebra, Foldable, Functor, Monad } from './index.js' -/** @import { Construct, Morphism, Set } from './common.js' */ +/** @import { Morphism } from './types.js' */ /** * @template T, E - * @typedef {Ok | Err} Result */ +export class Ok extends Algebra(Monad, Foldable) { + /** @type {T} */ + #value -/** - * @template T - * @typedef OkMethods - * @property {typeof isOk} isOk - * @property {typeof isErr} isErr - * @property {typeof chain} chain - * @property {typeof map} map - * @property {typeof alt} alt - * @property {typeof fold} fold - * @property {typeof bimap} bimap - */ + /** + * @param {T} value + * @constructs {Ok} + */ + constructor(value) { + super() + this.#value = value + } -/** - * @template T - * @typedef {Set & Construct & OkMethods} Ok - */ + /** @returns {this is Ok} */ + isOk() { return true } -/** - * @template T - * @typedef ErrMethods - * @property {typeof isOk} isOk - * @property {typeof isErr} isErr - * @property {typeof chain} chain - * @property {typeof map} map - * @property {typeof alt} alt - * @property {typeof fold} fold - * @property {typeof bimap} bimap - */ + /** @returns {this is Err} */ + isErr() { return false } -/** - * @template T - * @typedef {Set & Construct & ErrMethods} Err - */ + /** + * @template R + * @param {Morphism} f + * @this {Ok} + * @returns {R} + */ + chain(f) { + return f(this.#value) + } -/** - * @template T, U, E - * @this {Result} - * @param {Morphism} fn - * @returns {Result} - */ -function map(fn) { - return /** @type {Result} */ (this[Self](this.chain(fn))) -} + /** + * @template R + * @param {Morphism} f + * @this {Ok} + * @returns {Ok} + */ + map(f) { + return Result.of(this.chain(f)) + } -/** - * @template T1, T2, E1, E2 - * @this {Result} - * @param {Morphism} y - * @param {Morphism} x - * @returns {Result} - */ -function bimap(x, y) { - return map.call(this, y).map(x) -} + /** + * @template R + * @param {Morphism} f + * @this {Ok} + * @returns {Ok} + */ + then(f) { + return this.map(f) + } -/** - * @template T, U, E - * @this {Result} - * @param {Morphism} fn - * @param {U} acc - * @return {U} - */ -function fold(fn, acc) { - const result = this.map(fn) - return result.isOk() ? result[Value] : acc + /** + * @template R + * @param {Morphism} _f + * @returns {this} + */ + catch(_f) { + return this + } + + /** + * @template U + * @param {(acc: U, value: T) => U} f + * @param {U} init + * @returns {U} + */ + reduce(f, init) { + return f(init, this.#value) + } } /** * @template T, E - * @this {Result} - * @param {Result} other - * @returns {Result} */ -function alt(other) { - return this.isOk() ? this : other +export class Err extends Algebra(Functor, Monad) { + /** @type {E} */ + #value + + + /** + * @param {E} value + * @constructs {Err} + */ + constructor(value) { + super() + this.#value = value + } + + /** @returns {this is Ok} */ + isOk() { return false } + + /** @returns {this is Err} */ + isErr() { return true } + + /** + * @template R + * @param {Morphism} _f + * @returns {this} + */ + chain(_f) { + return this + } + + /** + * @template R + * @param {Morphism} _f + * @returns {this} + */ + map(_f) { + return this + } + + /** + * @template R + * @param {Morphism} _f + * @returns {this} + */ + then(_f) { + return this + } + + /** + * @template R + * @param {Morphism} f + * @returns {Err} + */ + catch(f) { + return new Err(f(this.#value)) + } + + /** + * @template U + * @param {(acc: U, value: T) => U} _f + * @param {U} init + * @returns {U} + */ + reduce(_f, init) { + return init + } } /** * @template T, E - * @this {Result} - * @returns {this is Ok} + * @type {Ok | Err} Result */ -function isOk() { - return this[Self] === Ok +export class Result { + /** + * @template T + * @param {T} value + * @returns {Ok} + */ + static of(value) { + return new Ok(value) + } } /** * @template T, E - * @this {Result} - * @returns {this is Err} + * @param {T} v + * @returns {Ok} */ -function isErr() { - return this[Self] === Err -} - -/** - * @template T - * @param {?T} value - * @returns {Ok} - */ -export const Ok = value => Object.freeze({ - [Self]: Ok, - [Value]: value, - isOk, - isErr, - chain, - map, - alt, - fold, - bimap -}) - -/** - * @template T - * @param {T?} value - * @returns {Err} - */ -export const Err = value => Object.freeze({ - [Self]: Err, - [Value]: value, - chain: constant, - map: constant, - alt, - isOk, - isErr, - fold, - bimap -}) +export const ok = v => new Ok(v) /** * @template T, E - * @param {T} value - * @returns {Result} + * @param {E} e + * @returns {Err} */ -export const Result = value => Ok(value) -Result.of = Result -Result.zero = () => Err(undefined) +export const err = e => new Err(e) diff --git a/src/algebra/types.js b/src/algebra/types.js new file mode 100644 index 0000000..a3690c7 --- /dev/null +++ b/src/algebra/types.js @@ -0,0 +1,33 @@ +export default {} + +/** + * @template T, R + * @typedef {(value: T) => R} Morphism + */ + +/** + * @template T + * @typedef {(value: T) => R} InferredMorphism + */ + +/** + * @template T + * @typedef {(value: T) => boolean} Predicate + */ + +/** + * @template T + * @typedef {(value: T) => Chain} ChainConstructor + */ + +/** + * @template T + * @typedef {(f: Morphism) => R} chain + */ + +/** + * @template T + * @typedef {{ chain: chain }} Chain + */ + +/** @typedef {(...args: any[]) => any} Fn */ diff --git a/src/mixin.js b/src/mixin.js new file mode 100644 index 0000000..89fbe27 --- /dev/null +++ b/src/mixin.js @@ -0,0 +1,256 @@ +/** + * mixwith.js + * Author: Justin Fagnani (https://github.com/justinfagnani/) + * https://github.com/justinfagnani/mixwith.js + */ + +'use strict' + +// used by apply() and isApplicationOf() +const _appliedMixin = '__mixwith_appliedMixin' + +/** + * A function that returns a subclass of its argument. + * + * @example + * const M = (superclass) => class extends superclass { + * getMessage() { + * return "Hello" + * } + * } + * + * @typedef {Function} MixinFunction + * @param {Function} superclass + * @return {Function} A subclass of `superclass` + */ + +/** + * Applies `mixin` to `superclass`. + * + * `apply` stores a reference from the mixin application to the unwrapped mixin + * to make `isApplicationOf` and `hasMixin` work. + * + * This function is usefull for mixin wrappers that want to automatically enable + * {@link hasMixin} support. + * + * @example + * const Applier = (mixin) => wrap(mixin, (superclass) => apply(superclass, mixin)) + * + * // M now works with `hasMixin` and `isApplicationOf` + * const M = Applier((superclass) => class extends superclass {}) + * + * class C extends M(Object) {} + * let i = new C() + * hasMixin(i, M) // true + * + * @function + * @param {Function} superclass A class or constructor function + * @param {MixinFunction} mixin The mixin to apply + * @return {Function} A subclass of `superclass` produced by `mixin` + */ +export const apply = (superclass, mixin) => { + let application = mixin(superclass) + application.prototype[_appliedMixin] = unwrap(mixin) + return application +} + +/** + * Returns `true` iff `proto` is a prototype created by the application of + * `mixin` to a superclass. + * + * `isApplicationOf` works by checking that `proto` has a reference to `mixin` + * as created by `apply`. + * + * @function + * @param {Object} proto A prototype object created by {@link apply}. + * @param {MixinFunction} mixin A mixin function used with {@link apply}. + * @return {boolean} whether `proto` is a prototype created by the application of + * `mixin` to a superclass + */ +export const isApplicationOf = (proto, mixin) => + proto.hasOwnProperty(_appliedMixin) && proto[_appliedMixin] === unwrap(mixin) + +/** + * Returns `true` iff `o` has an application of `mixin` on its prototype + * chain. + * + * @function + * @param {Object} o An object + * @param {MixinFunction} mixin A mixin applied with {@link apply} + * @return {boolean} whether `o` has an application of `mixin` on its prototype + * chain + */ +export const hasMixin = (o, mixin) => { + while (o != null) { + if (isApplicationOf(o, mixin)) return true + o = Object.getPrototypeOf(o) + } + return false +} + + +// used by wrap() and unwrap() +const _wrappedMixin = '__mixwith_wrappedMixin' + +/** + * Sets up the function `mixin` to be wrapped by the function `wrapper`, while + * allowing properties on `mixin` to be available via `wrapper`, and allowing + * `wrapper` to be unwrapped to get to the original function. + * + * `wrap` does two things: + * 1. Sets the prototype of `mixin` to `wrapper` so that properties set on + * `mixin` inherited by `wrapper`. + * 2. Sets a special property on `mixin` that points back to `mixin` so that + * it can be retreived from `wrapper` + * + * @function + * @param {MixinFunction} mixin A mixin function + * @param {MixinFunction} wrapper A function that wraps {@link mixin} + * @return {MixinFunction} `wrapper` + */ +export const wrap = (mixin, wrapper) => { + Object.setPrototypeOf(wrapper, mixin) + if (!mixin[_wrappedMixin]) { + mixin[_wrappedMixin] = mixin + } + return wrapper +} + +/** + * Unwraps the function `wrapper` to return the original function wrapped by + * one or more calls to `wrap`. Returns `wrapper` if it's not a wrapped + * function. + * + * @function + * @param {MixinFunction} wrapper A wrapped mixin produced by {@link wrap} + * @return {MixinFunction} The originally wrapped mixin + */ +export const unwrap = (wrapper) => wrapper[_wrappedMixin] || wrapper + +const _cachedApplications = '__mixwith_cachedApplications' + +/** + * Decorates `mixin` so that it caches its applications. When applied multiple + * times to the same superclass, `mixin` will only create one subclass, memoize + * it and return it for each application. + * + * Note: If `mixin` somehow stores properties its classes constructor (static + * properties), or on its classes prototype, it will be shared across all + * applications of `mixin` to a super class. It's reccomended that `mixin` only + * access instance state. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap with caching behavior + * @return {MixinFunction} a new mixin function + */ +export const Cached = (mixin) => wrap(mixin, (superclass) => { + // Get or create a symbol used to look up a previous application of mixin + // to the class. This symbol is unique per mixin definition, so a class will have N + // applicationRefs if it has had N mixins applied to it. A mixin will have + // exactly one _cachedApplicationRef used to store its applications. + + let cachedApplications = superclass[_cachedApplications] + if (!cachedApplications) { + cachedApplications = superclass[_cachedApplications] = new Map() + } + + let application = cachedApplications.get(mixin) + if (!application) { + application = mixin(superclass) + cachedApplications.set(mixin, application) + } + + return application +}) + +/** + * Decorates `mixin` so that it only applies if it's not already on the + * prototype chain. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap with deduplication behavior + * @return {MixinFunction} a new mixin function + */ +export const DeDupe = (mixin) => wrap(mixin, (superclass) => + (hasMixin(superclass.prototype, mixin)) + ? superclass + : mixin(superclass)) + +/** + * Adds [Symbol.hasInstance] (ES2015 custom instanceof support) to `mixin`. + * + * @function + * @param {MixinFunction} mixin The mixin to add [Symbol.hasInstance] to + * @return {MixinFunction} the given mixin function + */ +export const HasInstance = (mixin) => { + if (Symbol && Symbol.hasInstance && !mixin[Symbol.hasInstance]) { + Object.defineProperty(mixin, Symbol.hasInstance, { + value(o) { + return hasMixin(o, mixin) + }, + }) + } + return mixin +} + +/** + * A basic mixin decorator that applies the mixin with {@link apply} so that it + * can be used with {@link isApplicationOf}, {@link hasMixin} and the other + * mixin decorator functions. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap + * @return {MixinFunction} a new mixin function + */ +export const BareMixin = (mixin) => wrap(mixin, (s) => apply(s, mixin)) + +/** + * Decorates a mixin function to add deduplication, application caching and + * instanceof support. + * + * @function + * @param {MixinFunction} mixin The mixin to wrap + * @return {MixinFunction} a new mixin function + */ +export const Mixin = (mixin) => DeDupe(Cached(BareMixin(mixin))) + +/** + * A fluent interface to apply a list of mixins to a superclass. + * + * ```javascript + * class X extends mix(Object).with(A, B, C) {} + * ``` + * + * The mixins are applied in order to the superclass, so the prototype chain + * will be: X->C'->B'->A'->Object. + * + * This is purely a convenience function. The above example is equivalent to: + * + * ```javascript + * class X extends C(B(A(Object))) {} + * ``` + * + * @function + * @param {Function} [superclass=Object] + * @return {MixinBuilder} + */ +export const mix = (superclass) => new MixinBuilder(superclass) + +class MixinBuilder { + + constructor(superclass) { + this.superclass = superclass || class { } + } + + /** + * Applies `mixins` in order to the superclass given to `mix()`. + * + * @param {Array.} mixins + * @return {FunctionConstructor} a subclass of `superclass` with `mixins` applied + */ + with(...mixins) { + return mixins.reduce((c, m) => m(c), this.superclass) + } +} + diff --git a/src/union.js b/src/union.js index 03a52b0..b5e6a39 100644 --- a/src/union.js +++ b/src/union.js @@ -10,7 +10,7 @@ /** * @template R - * @callback match + * @callback cata * @param {Record> & Partial>>} pattern * @returns {R} * @@ -19,7 +19,7 @@ /** * @typedef Variant - * @property {match} match + * @property {cata} cata */ /** @@ -39,21 +39,21 @@ const Tag = Symbol('Tag') -class MatchError extends Error { +export class CatamorphismError extends Error { /** @param {PropertyKey} name */ constructor(name) { - super(`unmatched arm in match: ${name.toString()}`) + super(`unmatched arm in catamorphism: ${name.toString()}`) } } /** - * @param {PropertyKey} variant + * @param {PropertyKey} type * @param {PropertyKey} variant * @returns {(...values: any[]) => Variant} */ const Variant = (type, variant) => (...values) => ({ [Tag]: type, - match(pattern) { + cata(pattern) { if (variant in pattern) { // NOTE: this is a workaround for typescript not recognizing // that objects can be indexed with symbols @@ -61,7 +61,7 @@ const Variant = (type, variant) => (...values) => ({ } else if ('_' in pattern) { return pattern._() } else { - throw new MatchError(variant) + throw new CatamorphismError(variant) } } })