move interfaces to their own file

This commit is contained in:
Rowan 2025-04-09 22:22:44 -05:00
parent 96cc50ad34
commit 35aa1c6ae9
14 changed files with 389 additions and 457 deletions

2
.gitmodules vendored
View file

@ -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

8
package-lock.json generated
View file

@ -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",

View file

@ -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<B>}
*/
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<C>}
*/
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<B>}
*/
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<B>}
*/
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<A, A>}
*/
export const pure = x => new Pure(x)
/**
* @template A, B
* @param {Computation<A, B>} 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))

View file

@ -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<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 */
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'

361
src/algebra/interfaces.js Normal file
View file

@ -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<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 */
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')

View file

@ -1,4 +1,4 @@
import { Algebra, Monad } from './index.js'
import { Algebra, Monad } from './interfaces.js'
/** @import { Fn, InferredMorphism, Morphism } from './types.js' */

View file

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

View file

@ -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<T, R>} _f
@ -132,6 +132,5 @@ export class Option {
* @param {T} value
*/
export const some = value => new Some(value)
export const none = new None()

View file

@ -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' */

View file

@ -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' */

View file

@ -1,3 +1,2 @@
export * from './algebra/index.js'
export * from './curry.js'

View file

@ -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())

View file

@ -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),

2
vendor/izuna vendored

@ -1 +1 @@
Subproject commit d9daed0d0977f7b79462fb204a5d89b827dcac1b
Subproject commit aa70427c8c349bbfe4576cba878f5b44859007d4