From 35aa1c6ae9e1320f37619c728ef33a123c885014 Mon Sep 17 00:00:00 2001 From: rowan Date: Wed, 9 Apr 2025 22:22:44 -0500 Subject: [PATCH] move interfaces to their own file --- .gitmodules | 2 +- package-lock.json | 8 +- src/algebra/free.js | 55 +----- src/algebra/index.js | 366 +------------------------------------- src/algebra/interfaces.js | 361 +++++++++++++++++++++++++++++++++++++ src/algebra/io.js | 2 +- src/algebra/list.js | 15 +- src/algebra/option.js | 5 +- src/algebra/reader.js | 4 +- src/algebra/result.js | 2 +- src/index.js | 1 - tests/index.js | 19 +- tests/units/monad.js | 4 +- vendor/izuna | 2 +- 14 files changed, 389 insertions(+), 457 deletions(-) create mode 100644 src/algebra/interfaces.js diff --git a/.gitmodules b/.gitmodules index 9b84117..53e220e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "vendor/izuna"] path = vendor/izuna - url = git@git.kitsu.cafe:rowan/izuna.git + url = https://git.kitsu.cafe/rowan/izuna.git diff --git a/package-lock.json b/package-lock.json index 9369ea7..41907ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,14 +17,14 @@ }, "node_modules/folktest": { "version": "1.0.0", - "resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#b130e6fd1839a32ca62ffe9c96da58d8bdf39b38", + "resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#708d44f1215be33fcceba426029f44b4f963dbe5", "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==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/src/algebra/free.js b/src/algebra/free.js index 6c04605..9bf132d 100644 --- a/src/algebra/free.js +++ b/src/algebra/free.js @@ -1,4 +1,4 @@ -import { id, map, ap, prepend, reduce, thunk } from './fn.js' +import { id, map, ap, prepend, reduce, thunk } from '../../vendor/izuna/src/index.js' /** @import { Morphism } from './types.js' */ @@ -24,7 +24,6 @@ export class Pure { * @returns {B} */ chain(f) { - console.log('Pure.chain', this.#value) return f(this.#value) } @@ -34,7 +33,6 @@ export class Pure { * @returns {Free} */ map(f) { - console.log(`Pure.map ${f} ${this.#value}`) // @ts-ignore return pure(f(this.#value)) } @@ -45,7 +43,6 @@ export class Pure { * @returns {Free} */ ap(b) { - console.log('Pure.ap', b, this.#value) return b.map(this.#value) } @@ -60,7 +57,6 @@ export class Pure { * @returns {B} */ reduce(f, init) { - console.log('Pure.reduce', init, this.#value) return f(init, this.#value) } @@ -88,15 +84,10 @@ export class Impure { * @returns {B} */ chain(f) { - console.log('Impure.chain') // @ts-ignore return new Impure( // @ts-ignore - () => { - console.log(`Impure.chain<${f}>`, this.#next()) - this.#next().chain(f) - } - //() => this.#next().chain(f) + () => this.#next().chain(f) ) } @@ -106,14 +97,9 @@ export class Impure { * @returns {Free} */ map(f) { - console.log(`Impure.map ${f}`) return new Impure( // @ts-ignore - () => { - console.log(`Impure.map<${f}>`, this.#next()) - return this.#next().map(f) - } - //() => this.#next().map(f) + () => this.#next().map(f) ) } @@ -123,7 +109,6 @@ export class Impure { * @returns {Free} */ ap(b) { - console.log('Impure.ap', b) return new Impure( // @ts-ignore () => b.map(this.#next()) @@ -148,45 +133,13 @@ export class Impure { } const sequence = (of, iter) => { - console.log(ap(of([]), 1)) return reduce((acc, x) => { ap(acc, map(prepend, x)) }, of([]), iter) } - -class Reducer { - constructor(v) { - this.value = v - } - - static of(v) { - return new Reducer(v) - } - - ap(other) { - console.log(this, other) - return this.value(other.value) - } -} - -const r = Reducer.of -console.log(sequence(r, [r(1), r(2), r(3)])) - -/** - * @template A - * @param {A} x - * @returns {Free} - */ export const pure = x => new Pure(x) - -/** - * @template A, B - * @param {Computation} f - */ export const impure = f => new Impure(f) + export const liftF = effect => impure(thunk(effect)) -const xs = liftF(pure(1)).map(thunk(2)).map(thunk(3)) -console.log(xs.traverse(id, id)) - diff --git a/src/algebra/index.js b/src/algebra/index.js index 37c71a1..0939c76 100644 --- a/src/algebra/index.js +++ b/src/algebra/index.js @@ -1,361 +1,7 @@ -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} 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 - - /** @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} */ - #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 - } -} - -/** @template T */ -export 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 {Function} base - * @returns {(...algebras: Interface[]) => FunctionConstructor} - */ -export const AlgebraWithBase = base => (...algebras) => { - return mix(base).with(...algebras.map(x => x.intoWrapper())) -} - -/** - * @param {...Interface} algebras - * @returns {FunctionConstructor} - */ -export const Algebra = AlgebraWithBase(BaseSet) - -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') +export * from './option.js' +export * from './result.js' +export * from './list.js' +export * from './free.js' +export * from './io.js' +export * from './reader.js' diff --git a/src/algebra/interfaces.js b/src/algebra/interfaces.js new file mode 100644 index 0000000..37c71a1 --- /dev/null +++ b/src/algebra/interfaces.js @@ -0,0 +1,361 @@ +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} 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 + + /** @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} */ + #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 + } +} + +/** @template T */ +export 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 {Function} base + * @returns {(...algebras: Interface[]) => FunctionConstructor} + */ +export const AlgebraWithBase = base => (...algebras) => { + return mix(base).with(...algebras.map(x => x.intoWrapper())) +} + +/** + * @param {...Interface} algebras + * @returns {FunctionConstructor} + */ +export const Algebra = AlgebraWithBase(BaseSet) + +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') + diff --git a/src/algebra/io.js b/src/algebra/io.js index 3fa4971..3061f4a 100644 --- a/src/algebra/io.js +++ b/src/algebra/io.js @@ -1,4 +1,4 @@ -import { Algebra, Monad } from './index.js' +import { Algebra, Monad } from './interfaces.js' /** @import { Fn, InferredMorphism, Morphism } from './types.js' */ diff --git a/src/algebra/list.js b/src/algebra/list.js index ec6da93..7ea988b 100644 --- a/src/algebra/list.js +++ b/src/algebra/list.js @@ -1,5 +1,4 @@ -import { Pure, Suspend } from './free.js' -import { Algebra, AlgebraWithBase, Comonad, Foldable, Monoid, Semigroup } from './index.js' +import { Algebra, Comonad, Foldable, Monoid, Semigroup } from './interfaces.js' /** @import { InferredMorphism, Morphism } from './types.js' */ @@ -188,15 +187,5 @@ export class List { const empty = new Empty() -/** - * @template T - * @param {T[]} acc - * @param {T} value - * @returns {T[]} - */ -const reduceArray = (acc, value) => acc.concat(value) - -const arr = Array.from({ length: 10 }) -const list = List.from(arr) -list.map(x => x * 2) +export const list = value => List.of(value) diff --git a/src/algebra/option.js b/src/algebra/option.js index 42d729b..8de9f80 100644 --- a/src/algebra/option.js +++ b/src/algebra/option.js @@ -1,4 +1,4 @@ -import { Algebra, Foldable, Monad, Monoid } from './index.js' +import { Algebra, Foldable, Monad, Monoid } from './interfaces.js' /** @import { Morphism } from './types.js' */ @@ -66,7 +66,7 @@ export class Some extends Algebra(Monoid, Monad, Foldable) { } /** @template T */ -class None extends Algebra(Monoid, Monad, Foldable) { +export class None extends Algebra(Monoid, Monad, Foldable) { /** * @template R * @param {Morphism} _f @@ -132,6 +132,5 @@ export class Option { * @param {T} value */ export const some = value => new Some(value) - export const none = new None() diff --git a/src/algebra/reader.js b/src/algebra/reader.js index 875a969..c3d4c6c 100644 --- a/src/algebra/reader.js +++ b/src/algebra/reader.js @@ -1,5 +1,5 @@ -import { id } from './fn.js' -import { Algebra, Monad } from './index.js' +import { id } from '../../vendor/izuna/src/index.js' +import { Algebra, Monad } from './interfaces.js' /** @import { InferredMorphism, Morphism } from './types.js' */ diff --git a/src/algebra/result.js b/src/algebra/result.js index 14a4f6c..f7a18f6 100644 --- a/src/algebra/result.js +++ b/src/algebra/result.js @@ -1,4 +1,4 @@ -import { Algebra, Foldable, Functor, Monad } from './index.js' +import { Algebra, Foldable, Functor, Monad } from './interfaces.js' /** @import { Morphism } from './types.js' */ diff --git a/src/index.js b/src/index.js index cce5240..b1f26e0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,2 @@ export * from './algebra/index.js' -export * from './curry.js' diff --git a/tests/index.js b/tests/index.js index d90cecb..d9b03db 100755 --- a/tests/index.js +++ b/tests/index.js @@ -1,22 +1,7 @@ #!/usr/bin/env node +import { TerminalRunner } from 'folktest' import * as Tests from './units/index.js' -const ap = f => f() -const fmt = ({ success, description, error }) => { - if (success) { - return `${description}: PASS` - } else { - return `${description}: FAIL\n${error.stack}` - } -} - -const results = Object.entries(Tests).map(([name, tests]) => ({ - name, - tests: tests.map(ap).map(fmt).join('\n') -})) - .map(({ name, tests }) => `${name}\n${tests}`) - .join('\n') - -console.log(results) +console.log(TerminalRunner(Tests).toString()) diff --git a/tests/units/monad.js b/tests/units/monad.js index 6959a53..cfdf2b2 100644 --- a/tests/units/monad.js +++ b/tests/units/monad.js @@ -1,5 +1,5 @@ import { it, assertEq } from 'folktest' -import { Identity, Constant, Ok, Err, Some, None } from '../../src/index.js' +import { Ok, Err, Some, None } from '../../src/index.js' const leftIdentity = (M, a, f) => { assertEq(M(a).bind(f), f(a)) @@ -21,7 +21,7 @@ export const prove = (M, m, a, f, g) => { export const Tests = [ it('should adhere to monadic laws', () => { - [Identity, Constant, Ok, Err, Some, None].forEach(ctr => { + [Ok, Err, Some, None].forEach(ctr => { prove( ctr, ctr(1), diff --git a/vendor/izuna b/vendor/izuna index d9daed0..aa70427 160000 --- a/vendor/izuna +++ b/vendor/izuna @@ -1 +1 @@ -Subproject commit d9daed0d0977f7b79462fb204a5d89b827dcac1b +Subproject commit aa70427c8c349bbfe4576cba878f5b44859007d4