diff --git a/src/algebra/free.js b/src/algebra/free.js index 9bf132d..536ed01 100644 --- a/src/algebra/free.js +++ b/src/algebra/free.js @@ -1,145 +1,145 @@ -import { id, map, ap, prepend, reduce, thunk } from '../../vendor/izuna/src/index.js' -/** @import { Morphism } from './types.js' */ +import { Algebra, Monad } from './interfaces.js' +import { curry } from '../..//vendor/izuna/src/index.js' +/** @import { Applicative, ApplicativeTypeRef, Morphism } from './types.js' */ + + +/** @import { InferredMorphism } from './types.js' */ + +const kleisli = curry((f, g, x) => f(x).chain(g)) /** - * @template A - * @typedef {Pure | Impure} Free + * @template T + * @type {Pure | Impure} Free */ +export class Free extends Algebra(Monad) { + /** + * @template T + * @param {T} value + */ + static of(value) { + return new Pure(value) + } +} -/** @template A */ -export class Pure { +/** @template T */ +export class Pure extends Free { #value /** - * @param {A} value + * @param {T} value */ constructor(value) { + super() this.#value = value } /** - * @template {Free} B - * @param {Morphism} f - * @returns {B} + * @param {InferredMorphism} f */ chain(f) { return f(this.#value) } /** - * @template B - * @param {Morphism} f - * @returns {Free} + * @param {InferredMorphism} f */ map(f) { - // @ts-ignore return pure(f(this.#value)) } /** - * @template B, C - * @param {Free} b - * @returns {Free} + * @template U + * @template {Applicative} M + * @template {ApplicativeTypeRef} TypeRef + * @param {TypeRef} _A + * @param {*} f + * @returns {M} */ - ap(b) { - return b.map(this.#value) + traverse(_A, f) { + return f(this.#value).map(pure) } - traverse(f, of) { - return of(this.map(f)) + run() { + return this.#value } - - /** - * @template B - * @param {(acc: B, value: A) => B} f - * @param {B} init - * @returns {B} - */ - reduce(f, init) { - return f(init, this.#value) - } - } -/** - * @template A - * @typedef {(value: A) => Free} Computation - */ - -/** @template A */ -export class Impure { +/** @template T */ +export class Impure extends Free { + #value #next /** - * @param {() => Free} next + * @param {T} value + * @param {(value: T) => Free} f */ - constructor(next) { - this.#next = next + constructor(value, f) { + super() + this.#value = value + this.#next = f } /** - * @template {Free} B - * @param {Morphism} f - * @returns {B} + * @param {(value: T) => Free} f */ chain(f) { - // @ts-ignore - return new Impure( - // @ts-ignore - () => this.#next().chain(f) - ) + return impure(this.#value, kleisli(this.#next, f)) } - /** - * @template B - * @param {Morphism} f - * @returns {Free} - */ map(f) { - return new Impure( - // @ts-ignore - () => this.#next().map(f) + return impure( + this.#value, + y => f(y).chain(f) ) } /** - * @template B - * @param {Free} b - * @returns {Free} + * @template U + * @template {Applicative} M + * @template {ApplicativeTypeRef} TypeRef + * @param {TypeRef} A + * @param {(value: T) => M} f + * @returns {M} */ - ap(b) { - return new Impure( - // @ts-ignore - () => b.map(this.#next()) + traverse(A, f) { + const fb = f(this.#value) + const rest = this.#next(this.#value).traverse(A, f) + + return rest.ap( + fb.map(b => next => impure(b, () => next)) ) } - traverse(f, of) { - return of(liftF( - this.#next().traverse(f, of) - )) - } - - /** - * @template B - * @param {(acc: B, value: A) => B} f - * @param {B} acc - * @returns {B} - */ - reduce(f, acc) { - + run() { + return this.#next(this.#value).run() } } -const sequence = (of, iter) => { - return reduce((acc, x) => { - ap(acc, map(prepend, x)) - }, of([]), iter) -} +/** + * @template T + * @param {T} x + * @returns {Pure} + */ +const pure = x => new Pure(x) -export const pure = x => new Pure(x) -export const impure = f => new Impure(f) +/** + * @template T + * @param {T} x + * @param {(value: T) => Free} f + * @returns {Impure} + */ +const impure = (x, f) => new Impure(x, f) -export const liftF = effect => impure(thunk(effect)) +/** + * @template T + * @param {T} x + * @returns Impure + */ +export const liftF = x => impure(x, pure) +import { id, Identity } from './identity.js' +import { Option, some } from './option.js' + +const a = liftF(1).chain(() => liftF(2)).traverse(Option, x => some(1)) +console.log(a.chain(x => x).run()) diff --git a/src/algebra/identity.js b/src/algebra/identity.js new file mode 100644 index 0000000..d892b41 --- /dev/null +++ b/src/algebra/identity.js @@ -0,0 +1,65 @@ +import { Algebra, Comonad, Monad } from './interfaces.js' + +/** @import { Morphism } from './types.js' */ + +/** @template T */ +export class Identity extends Algebra(Monad, Comonad) { + #value + + /** + * @param {T} value + */ + constructor(value) { + super() + + this.#value = value + } + + /** + * @template T + * @param {T} value + */ + static of(value) { + return id(value) + } + + /** + * @template U + * @param {Morphism} f + */ + map(f) { + return id(f(this.#value)) + } + + /** + * @template U + * @this {Identity>} + * @param {Identity} b + * @returns {Identity} + */ + ap(b) { + return b.map(this.#value) + } + + /** + * @template U + * @param {Morphism>} f + */ + chain(f) { + return f(this.#value) + } + + /** + * @param {(value: Identity) => T} f + */ + extend(f) { + return id(f(this)) + } + + extract() { + return this.#value + } +} + +export const id = value => new Identity(value) + diff --git a/src/algebra/interfaces.js b/src/algebra/interfaces.js index 08b6b9d..f877198 100644 --- a/src/algebra/interfaces.js +++ b/src/algebra/interfaces.js @@ -1,4 +1,4 @@ -import { mix } from '../mixin.js' +import { mix, Mixin } from '../mixin.js' /** @import { MixinFunction } from '../mixin.js' */ /** @import { Fn } from './types.js' */ @@ -130,7 +130,7 @@ class Method { */ _getInstallationPoint(target) { switch (this.#type) { - case 0: return target.prototype + case 0: return Object.getPrototypeOf(target) case 1: return target default: return target } @@ -152,6 +152,9 @@ const Implementations = Symbol() class Interface { #name + /** @type {MixinFunction} */ + #mixin + /** @type {Set} */ #methods = new Set() @@ -161,20 +164,20 @@ class Interface { /** @param {string} name */ constructor(name) { this.#name = name + this.#mixin = Mixin(this.build.bind(this)) } get name() { return this.#name } - findInterfaces() { + findInterfaces(type) { let interfaces = new Set() - let current = Object.getPrototypeOf(this) - console.log(current) + let current = type while (current != null) { - interfaces = new Set(current[Implementations]) - current = Object.getPrototypeOf(this) + interfaces = new Set(current[Implementations]).union(interfaces) + current = Object.getPrototypeOf(current) } return interfaces @@ -185,7 +188,7 @@ class Interface { * @returns {boolean} */ implementedBy(type) { - return this.findInterfaces().has(type) + return this.findInterfaces(type).has(this) } /** @@ -212,10 +215,10 @@ class Interface { } /** - * @returns {MixinWrapper} + * @returns {MixinFunction} */ - intoWrapper() { - return this.build.bind(this) + asMixin() { + return this.#mixin } /** @@ -223,19 +226,20 @@ class Interface { */ build(base) { const interfaces = [...this.#interfaces.values()] - const wrappers = interfaces.map(x => x.intoWrapper()) + const mixins = interfaces.map(x => x.asMixin()) - const Interfaces = mix(base).with(...wrappers) + const Interfaces = mix(base).with(...mixins) - //const Algebra = class extends Interfaces { } + const Algebra = class extends Interfaces { } for (const method of this.#methods) { - method.implement(this, Interfaces) + method.implement(this, Algebra) } - Interfaces.prototype[Implementations] = new Set([...interfaces]) + const prototype = Object.getPrototypeOf(Algebra) + prototype[Implementations] = new Set(interfaces) - return Interfaces + return Algebra } } @@ -272,7 +276,7 @@ export const apply = (methodName, f, obj) => { * @returns {(...algebras: Interface[]) => FunctionConstructor} */ export const AlgebraWithBase = base => (...algebras) => { - return mix(base).with(...algebras.map(x => x.intoWrapper())) + return mix(base).with(...algebras.map(x => x.asMixin())) } /** diff --git a/src/algebra/option.js b/src/algebra/option.js index 945431d..60d8a21 100644 --- a/src/algebra/option.js +++ b/src/algebra/option.js @@ -2,8 +2,22 @@ import { Algebra, Foldable, Monad, Monoid, Setoid } from './interfaces.js' /** @import { Morphism } from './types.js' */ +/** + * @template T + * @type {Some | None} Option + */ +export class Option extends Algebra(Setoid, Monad, Foldable) { + /** + * @template T + * @param {T} value + */ + static of(value) { + return Some.of(value) + } +} + /** @template T */ -export class Some extends Algebra(Setoid, Monoid, Monad, Foldable) { +export class Some extends Option { /** @type {T} */ #value @@ -23,11 +37,18 @@ export class Some extends Algebra(Setoid, Monoid, Monad, Foldable) { return new Some(value) } + /** + * @param {Option} other + */ equals(other) { return other instanceof Some && other.chain(v => v === this.#value) } + ap(b) { + return b.chain(f => this.map(f)) + } + /** * @template R * @param {Morphism} f @@ -64,18 +85,21 @@ export class Some extends Algebra(Setoid, Monoid, Monad, Foldable) { reduce(f, init) { return f(init, this.#value) } - - empty() { - return Option.empty() - } } /** @template T */ -export class None extends Algebra(Setoid, Monoid, Monad, Foldable) { +export class None extends Option { + /** + * @param {Option} other + */ equals(other) { return other === none } + ap(_b) { + return this + } + /** * @template R * @param {Morphism} _f @@ -118,24 +142,6 @@ export class None extends Algebra(Setoid, Monoid, Monad, Foldable) { } } -/** - * @template T - * @type {Some | None} Option - */ -export class Option { - /** - * @template T - * @param {T} value - */ - static of(value) { - return Some.of(value) - } - - static empty() { - return none - } -} - /** * @template T * @param {T} value diff --git a/src/algebra/result.js b/src/algebra/result.js index aef2458..da59344 100644 --- a/src/algebra/result.js +++ b/src/algebra/result.js @@ -1,11 +1,26 @@ -import { Algebra, Foldable, Functor, Monad, Setoid } from './interfaces.js' +import { Algebra, Foldable, Monad, Setoid } from './interfaces.js' /** @import { Morphism } from './types.js' */ /** * @template T, E + * @type {Ok | Err} Result */ -export class Ok extends Algebra(Setoid, Monad, Foldable) { +export class Result extends Algebra(Setoid, Monad, Foldable) { + /** + * @template T + * @param {T} value + * @returns {Ok} + */ + static of(value) { + return new Ok(value) + } +} + +/** + * @template T, E + */ +export class Ok extends Result { /** @type {T} */ #value @@ -20,7 +35,7 @@ export class Ok extends Algebra(Setoid, Monad, Foldable) { } /** - * @param {Ok} other + * @param {Result} other */ equals(other) { return other instanceof Ok && @@ -86,7 +101,7 @@ export class Ok extends Algebra(Setoid, Monad, Foldable) { /** * @template T, E */ -export class Err extends Algebra(Setoid, Functor, Monad) { +export class Err extends Result { /** @type {E} */ #value @@ -100,6 +115,9 @@ export class Err extends Algebra(Setoid, Functor, Monad) { this.#value = value } + /** + * @param {Result} other + */ equals(other) { return other instanceof Err && other.chain(v => v === this.#value) @@ -158,21 +176,6 @@ export class Err extends Algebra(Setoid, Functor, Monad) { } } -/** - * @template T, E - * @type {Ok | Err} Result - */ -export class Result { - /** - * @template T - * @param {T} value - * @returns {Ok} - */ - static of(value) { - return new Ok(value) - } -} - /** * @template T, E * @param {T} v diff --git a/src/algebra/types.js b/src/algebra/types.js index a3690c7..d8b95d6 100644 --- a/src/algebra/types.js +++ b/src/algebra/types.js @@ -17,17 +17,197 @@ export default {} /** * @template T - * @typedef {(value: T) => Chain} ChainConstructor + * @typedef {{ + equals: (b: Setoid) => boolean + * }} Setoid */ /** * @template T - * @typedef {(f: Morphism) => R} chain + * @typedef {{ + lte: (b: Ord) => boolean + * }} Ord + */ + +/** + * @template T, U + * @typedef {*} Semigroupoid + */ + +/** + * @template I, J + * @typedef {{ + compose: (b: Semigroupoid) => Semigroupoid + * }} Category + */ + +/** + * @template T, U + * @template {Category} M + * @typedef {{ + id: () => (value: T) => M + * }} CategoryTypeRef */ /** * @template T - * @typedef {{ chain: chain }} Chain + * @typedef {{ + concat: (b: Semigroup) => Semigroup + * }} Semigroup + */ + +/** + * @template T + * @typedef{Semigroup} Monoid + */ + +/** + * @template T + * @template {Monoid} M + * @typedef {{ + empty: () => M + * }} MonoidTypeRef + */ + +/** + * @template T + * @typedef { + Monoid & + { invert: () => Group } + * } Group + */ + +/** + * @template T + * @typedef {{ + filter: (f: (acc: U, val: T) => U, init: U) => U + * }} Filterable + */ + +/** + * @template T + * @typedef {{ + map: (f: Morphism) => Functor + * }} Functor + */ + +/** + * @template T + * @typedef {{ + contramap: (f: Morphism) => Contravariant + * }} Contravariant + */ + +/** + * @template T + * @typedef {{ + ap: (f: Apply>) => Apply + * }} Apply + */ + +/** + * @template T + * @typedef {{ + ap: (b: Applicative>) => Applicative, + map: (f: Morphism) => Applicative + * }} Applicative + */ + +/** + * @template T + * @template {Applicative} M + * @typedef {{ + of: (value: T) => M + * }} ApplicativeTypeRef + */ + +/** + * @template T + * @typedef { + Functor & + { alt: (b: Alt) => Alt } + * } Alt + */ + +/** + * @template T + * @typedef {Alt} Plus + */ + +/** + * @template T + * @template {Plus} M + * @typedef { + Alt & + { zero: () => M } + * } PlusTypeRef + */ + +/** + * @template T + * @typedef {Applicative & Plus} Alternative + */ + +/** + * @template T + * @template {Applicative & Plus} M + * @typedef {ApplicativeTypeRef & PlusTypeRef} AlternativeTypeRef + */ + +/** + * @template T + * @typedef {{ + filter: (f: (acc: U, val: T) => U, init: U) => U + * }} Foldable + */ + +/** + * @template T + * @typedef { + Functor & Foldable & + { traverse: (A: ApplicativeTypeRef>, f: (val: T) => Applicative) => Applicative> } + * } Traversable + */ + +/** + * @template T + * @typedef { + Apply & + { chain: (f: (value: T) => Chain) => Chain } + * } Chain + */ + +/** + * @template T + * @typedef {Chain} ChainRec + */ + +/** + * @template T + * @typedef {Functor & Applicative & Chain} Monad + */ + +/** + * @template T + * @template {Monad} M + * @typedef {ApplicativeTypeRef} MonadTypeDef + */ + +/** + * @template T + * @typedef { + Functor & + { extend: (f: (val: Extend) => U) => Extend} + * } Extend + */ + +/** + * @template T + * @typedef { + Extend & + { extract: () => T } + * } Comonad */ /** @typedef {(...args: any[]) => any} Fn */ + diff --git a/src/mixin.js b/src/mixin.js index 89fbe27..31fe6ad 100644 --- a/src/mixin.js +++ b/src/mixin.js @@ -246,7 +246,7 @@ class MixinBuilder { /** * Applies `mixins` in order to the superclass given to `mix()`. * - * @param {Array.} mixins + * @param {Array.} mixins * @return {FunctionConstructor} a subclass of `superclass` with `mixins` applied */ with(...mixins) { diff --git a/tests/units/monad.js b/tests/units/monad.js index 6ce7ed4..c335d5f 100644 --- a/tests/units/monad.js +++ b/tests/units/monad.js @@ -1,28 +1,29 @@ // @ts-nocheck import { it, assert } from 'folktest' -import { Option, Result } from '../../src/index.js' -import { Chain, Ord, Setoid } from '../../src/algebra/interfaces.js' +import { IO, List, Option, Reader, Result, Some } from '../../src/index.js' +import { Applicative, Chain, Ord, Setoid } from '../../src/algebra/interfaces.js' -const Algebra = [Option, Result] +const Algebra = [Option, Result, List, IO, Reader] -const inc = x => F.of(x + 1) -const dbl = x => F.of(x * 2) +const inc = F => x => F.of(x + 1) +const dbl = F => x => F.of(x * 2) const impl = i => m => i.implementedBy(m) export const Tests = [ it('unit is a left identity', () => { - console.log(Chain.implementedBy(Result)) - Algebra.filter(impl(Chain)).forEach(algebra => { + Algebra.filter(impl(Setoid, Applicative, Chain)).forEach(algebra => { + const f = inc(algebra) + assert( - algebra.of(1).chain(inc) == inc(1), + algebra.of(1).chain(f).equals(f(1)), `${algebra.name} is not a left identity` ) }) }), it('unit is a right identity', () => { - Algebra.filter(impl(Chain)).forEach(algebra => { + Algebra.filter(impl(Setoid, Applicative, Chain)).forEach(algebra => { const m = algebra.of(1) assert( @@ -33,36 +34,15 @@ export const Tests = [ }), it('unit is associative', () => { - Algebra.filter(impl(Chain)).forEach(algebra => { + Algebra.filter(impl(Setoid, Applicative, Chain)).forEach(algebra => { const a = algebra.of(1) + const f = inc(algebra) + const g = dbl(algebra) - const x = a.chain(inc).chain(dbl) - const y = a.chain(x => inc(x).chain(dbl)) + const x = a.chain(f).chain(g) + const y = a.chain(x => f(x).chain(g)) - assert(x == y, `${algebra.name} is not associative`) + assert(x.equals(y), `${algebra.name} is not associative`) }) }), - - it('unit is reflexive', () => { - Algebra.filter(impl(Setoid)).forEach(algebra => { - const a = algebra.of(1) - assert(a.equals(a), `${algebra.name} is not reflexive`) - }) - }), - - it('unit is symmetrical', () => { - Algebra.filter(impl(Setoid)).forEach(algebra => { - const a = algebra.of(1) - const b = algebra.of(1) - assert(a.equals(b) && b.equals(a), `${algebra.name} is not symmetrical`) - }) - }), - - it('unit is antisymmetrical', () => { - Algebra.filter(impl(Ord)).forEach(algebra => { - const a = algebra.of(1) - const b = algebra.of(1) - assert(a.lte(b) && b.lte(a) && a.equals(b)) - }) - }) ]