kojima/src/algebra/index.js

361 lines
6.8 KiB
JavaScript

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')