import { mix } from '../mixin.js' /** @import { MixinFunction } from '../mixin.js' */ /** @import { Fn } from './types.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<readonly [any, any]>} iterable */ constructor(iterable) { this.#inner = new Map(iterable) } /** * @param {Iterable<any>} keys * @param {any} value * @returns {this} */ set(keys, value) { this.#inner.set(new Set(keys), value) return this } /** * @param {Iterable<any>} keys * @returns {boolean} */ has(keys) { return this.#inner.has(new Set(keys)) } /** * @param {Iterable<any>} keys */ get(keys) { return this.#inner.get(new Set(keys)) } /** * @param {Iterable<any>} 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 /** @type {Fn} */ #implementation /** * @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 {Fn} f */ implementation(f) { this.#implementation = f return this } /** * @param {string} interfaceName */ _defaultImplementation(interfaceName) { const err = new NotImplementedError( `${interfaceName}::${this.#name}` ) return function() { throw err } } /** * @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 impl = this.#implementation || this._defaultImplementation(builder.name) this._getInstallationPoint(target)[this.#name] = impl } } class Interface { #name /** @type {Set<Method>} */ #methods = new Set() /** @type {Set<Interface>} */ #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 } } /** @template T*/ class BaseSet { _value /** @param {T} value */ constructor(value) { this._value = value } } /** * @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( Method.from('chain') .implementation(function(f) { return f(this._value) })) ) 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')