This commit is contained in:
Rowan 2025-04-05 22:39:39 -05:00
parent d2051d9086
commit 699a8e6a7c
13 changed files with 1138 additions and 284 deletions

View file

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"module": "es2020", "module": "es2020",
"target": "es6", "target": "es6",
"lib": ["es2019", "dom"], "lib": ["es2022", "dom"],
"checkJs": true, "checkJs": true,
"paths": { "paths": {
"/*": ["./*"] "/*": ["./*"]

16
package-lock.json generated
View file

@ -8,6 +8,9 @@
"name": "kojima", "name": "kojima",
"version": "1.0.0", "version": "1.0.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": {
"typescript": "^5.8.2"
},
"devDependencies": { "devDependencies": {
"folktest": "git+https://git.kitsu.cafe/rowan/folktest.git" "folktest": "git+https://git.kitsu.cafe/rowan/folktest.git"
} }
@ -17,6 +20,19 @@
"resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#b130e6fd1839a32ca62ffe9c96da58d8bdf39b38", "resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#b130e6fd1839a32ca62ffe9c96da58d8bdf39b38",
"dev": true, "dev": true,
"license": "GPL-3.0-or-later" "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==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
} }
} }
} }

View file

@ -8,9 +8,17 @@
"scripts": { "scripts": {
"test": "./tests/index.js" "test": "./tests/index.js"
}, },
"keywords": ["functional", "functional programming", "fp", "monad"], "keywords": [
"functional",
"functional programming",
"fp",
"monad"
],
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"folktest": "git+https://git.kitsu.cafe/rowan/folktest.git" "folktest": "git+https://git.kitsu.cafe/rowan/folktest.git"
},
"dependencies": {
"typescript": "^5.8.2"
} }
} }

View file

@ -1,56 +0,0 @@
export const Self = Symbol()
export const Value = Symbol()
export function constant() { return this }
/**
* @template T, U
* @typedef {(x: T) => U} Morphism
*/
/**
* @template T
* @typedef {{ [Value]: T }} Set
*/
/**
* @template T
* @this {Construct<T>}
* @typedef {{ [Self]: <a>(value: a) => Construct<a> }} Construct
*/
/**
* @template T
* @param {T} value
* @returns {Set<T>}
*/
export const Set = value => ({ [Value]: value })
/**
* @template T, U
* @param {Morphism<T, U>} fn
* @returns {U}
*/
export function chain(fn) {
return fn(this[Value])
}
/**
* @template T
* @this {Set<T>}
* @returns {T}
*/
export function extract() {
return this[Value]
}
/**
* @template T, U
* @this {Construct<T>}
* @param {U} value
* @returns {Construct<U>}
*/
export function of(value) {
return this[Self](value)
}

45
src/algebra/fn.js Normal file
View file

@ -0,0 +1,45 @@
import { curry } from '../curry.js'
import { Free, Suspend, pure } from './free.js'
/** @import {InferredMorphism, Morphism, ChainConstructor} from './types.js' */
/**
* @template T
* @param {T} x
* @returns {x}
*/
export const id = x => x
export const compose = curry(
/**
* @template T, U, V
* @param {Morphism<U, V>} f
* @param {Morphism<T, U>} g
* @param {T} x
* @returns V
*/
(f, g, x) => f(g(x))
)
export const kleisliCompose = curry(
/**
* @template {ChainConstructor<T>} M, T, U
* @param {M} f
* @param {Morphism<T, U>} g
* @param {T} x
* @returns U
*/
(f, g, x) => f(x).chain(g)
)
/**
* @template T
* @param {T} x
* @returns {Free<T>}
*/
export const liftF = x => new Suspend(
x,
/** @type {InferredMorphism<T>} */(pure)
)

140
src/algebra/free.js Normal file
View file

@ -0,0 +1,140 @@
import { liftF } from './fn.js'
import { Algebra, Comonad, EitherOf, Functor, Monad } from './index.js'
/** @import { InferredMorphism, Morphism } from './types.js' */
/** @template T */
export class Suspend extends Algebra(Functor, Monad) {
#value
#fn
/**
* @param {T} value
* @param {InferredMorphism<T>} fn
*/
constructor(value, fn) {
super()
this.#value = value
this.#fn = fn
}
/** @returns {this is Pure<T>} */
isPure() { return false }
/** @returns {this is Suspend<T>} */
isSuspend() { return true }
/**
* @template U
* @param {Morphism<T, Free<U>>} f
* @returns {Suspend<T>}
*/
chain(f) {
return new Suspend(this.#value, x => this.#fn(x).chain(f))
}
/**
* @template U
* @param {Morphism<T, U>} g
* @returns {Suspend<T>}
*/
map(g) {
return new Suspend(this.#value, x => this.#fn(x).map(g))
}
/**
* @template U
* @param {Morphism<T, U>} g
* @returns {Suspend<T>}
*/
then(g) {
return this.map(g)
}
run() {
return this.#fn(this.#value)
}
}
/** @template T */
export class Pure extends Algebra(Functor, Monad, Comonad) {
#value
/** @param {T} value */
constructor(value) {
super()
this.#value = value
}
/** @returns {this is Pure<T>} */
isPure() { return true }
/** @returns {this is Suspend<T>} */
isSuspend() { return false }
/**
* @template U
* @param {Morphism<T, Pure<U>>} f
* @returns {Pure<U>}
*/
chain(f) {
return f(this.#value)
}
/**
* @template U
* @param {Morphism<T, U>} f
* @returns {Pure<U>}
*/
map(f) {
return this.chain(x => new Pure(f(x)))
}
/**
* @template R
* @param {Morphism<T, R>} f
* @returns {Pure<R>}
*/
then(f) {
return this.map(f)
}
extract() {
return this.#value
}
run() {
return this.#value
}
}
/**
* @template T
* @type {Pure<T> | Suspend<T>} Free
*/
export class Free {
/**
* @template T
* @param {T} value
* @returns {Free<T>}
*/
static of(value) {
return liftF(value)
}
}
/**
* @template T
* @param {T} value
* @returns {Pure<T>}
*/
export const pure = value => new Pure(value)
/**
* @template T
* @param {T} value
* @param {InferredMorphism<T>} f
* @returns {Suspend<T>}
*/
export const suspend = (value, f) => new Suspend(value, f)

View file

@ -1,3 +1,330 @@
export { Some, None, Option } from './option.js' import { mix } from '../mixin.js'
export { Ok, Err, Result } from './result.js'
/** @import { MixinFunction } from '../mixin.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
/**
* @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 {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 err = new NotImplementedError(
`${builder.name}::${this.#name}`
)
this._getInstallationPoint(target)[this.#name] = function() {
throw err
}
}
}
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
}
}
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 {...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('chain')
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')

54
src/algebra/list.js Normal file
View file

@ -0,0 +1,54 @@
import { liftF } from './fn.js'
import { Free } from './free.js'
import { Algebra, Foldable, Functor, Monad } from './index.js'
import { none as None } from './option.js'
/** @template T */
export class List extends Algebra(Functor, Monad, Foldable) {
/** @type {Free<T> | None<T>} */
_head
/** @type {List<T>} */
_tail
/**
* @param {T | None} [head]
* @param {List<T>} [tail=List]
*/
constructor(head = None, tail = List.of(None)) {
super()
this._head = head
this._tail = tail
}
/**
* @template T
* @param {T} value
* @returns {List<T>}
*/
prepend(value) {
return new List(value, this)
}
/**
* @template T
* @param {T} value
* @returns {List<T>}
*/
static of(value) {
return new List(value)
}
/**
* @template T
* @param {Iterable<T>} iterable
* @returns {List<T>}
*/
static from(iterable) {
Array.from(iterable).reduceRight((list, value))
}
}
const list = liftF(1)
console.log(list.map(_ => 2).map(_ => 3).chain(x => x).run())

View file

@ -1,122 +1,125 @@
import { chain, constant, Self, Value } from './common.js' import { Algebra, Foldable, Functor, Monad } from './index.js'
/** @import { Construct, Morphism, Set } from './common.js' */ /** @import { Morphism } from './types.js' */
/** /** @template T */
export class Some extends Algebra(Monad, Foldable) {
/** @type {T} */
#value
/** @param {T} value */
constructor(value) {
super()
this.#value = value
}
/**
* @template T * @template T
* @typedef {Some<T> | None} Option * @param {T} value
*/
/**
* @template T
* @typedef SomeMethods
* @property {typeof isSome} isSome
* @property {typeof isNone} isNone
* @property {typeof chain} chain
* @property {typeof map} map
* @property {typeof alt} alt
* @property {typeof fold} fold
*/
/**
* @template T
* @typedef {Construct<T> & Set<T> & SomeMethods<T>} Some
* @variation 1
*/
/**
* @typedef None
* @property {typeof isSome} isSome
* @property {typeof isNone} isNone
* @property {typeof chain} chain
* @property {typeof map} map
* @property {typeof alt} alt
* @property {typeof fold} fold
*/
/**
* @template T, U
* @this {Option<T>}
* @param {Morphism<T, U>} fn
* @returns {Option<U>}
*/
function map(fn) {
return this[Self](this.chain(fn))
}
/**
* @template T, U
* @this {Option<T>}
* @param {Morphism<T, U>} fn
* @param {U} acc
* @return {U}
*/
function fold(fn, acc) {
const result = this.map(fn)
return result.isSome() ? result[Value] : acc
}
/**
* @template T
* @this {Option<T>}
* @param {Option<T>} other
* @returns {Option<T>}
*/
function alt(other) {
return this.isSome() ? this : other
}
/**
* @template T
* @this {Option<T>}
* @returns {this is Some<T>}
*/
function isSome() {
return this[Self] === Some
}
/**
* @template T
* @this {Option<T>}
* @returns {this is None}
*/
function isNone() {
return this === None
}
/**
* @template T
* @param {?T} value
* @returns {Some<T>} * @returns {Some<T>}
*/ */
export const Some = value => ({ static of(value) {
[Self]: Some, return new Some(value)
[Value]: value, }
isSome,
isNone,
chain,
map,
alt,
fold
})
/** /**
* @template R
* @param {Morphism<T, R>} f
* @returns {R}
*/
chain(f) {
return f(this.#value)
}
/**
* @template R
* @param {Morphism<T, R>} f
* @returns {Some<R>}
*/
map(f) {
return Some.of(this.chain(f))
}
/**
* @template R
* @param {Morphism<T, R>} f
* @returns {Some<R>}
*/
then(f) {
return this.map(f)
}
/**
* @template U
* @param {(acc: U, value: T) => U} f
* @param {U} init
* @returns {U}
*/
reduce(f, init) {
return f(init, this.#value)
}
}
/** @template T */
class None extends Algebra(Monad, Foldable) {
/**
* @template R
* @param {Morphism<T, R>} _f
* @returns {None} * @returns {None}
*/ */
export const None = () => None chain(_f) {
None.isSome = isSome return this
None.isNone = isNone }
None.chain = constant
None.map = constant /**
None.alt = alt * @template R
None.fold = fold * @param {Morphism<T, R>} _f
* @returns {None}
*/
map(_f) {
return this
}
/**
* @template R
* @param {Morphism<T, R>} _f
* @returns {None}
*/
then(_f) {
return this
}
/**
* @template U
* @param {(acc: U, value: T) => U} _f
* @param {U} init
* @returns {U}
*/
reduce(_f, init) {
return init
}
}
/** /**
* @template T * @template T
* @param {?T} value * @type {Some<T> | None<T>} Option
* @returns {Option<T>}
*/ */
export const Option = value => Some(value) export class Option {
Option.of = Option /**
Option.zero = None * @template T
* @param {T} value
*/
static of(value) {
return Some.of(value)
}
}
/**
* @template T
* @param {T} value
*/
export const some = value => new Some(value)
export const none = new None()

View file

@ -1,147 +1,175 @@
import { chain, constant, Self, Value } from './common.js' import { Algebra, Foldable, Functor, Monad } from './index.js'
/** @import { Construct, Morphism, Set } from './common.js' */ /** @import { Morphism } from './types.js' */
/** /**
* @template T, E * @template T, E
* @typedef {Ok<T> | Err<E>} Result
*/ */
export class Ok extends Algebra(Monad, Foldable) {
/** @type {T} */
#value
/** /**
* @template T
* @typedef OkMethods
* @property {typeof isOk} isOk
* @property {typeof isErr} isErr
* @property {typeof chain} chain
* @property {typeof map} map
* @property {typeof alt} alt
* @property {typeof fold} fold
* @property {typeof bimap} bimap
*/
/**
* @template T
* @typedef {Set<T> & Construct<T> & OkMethods<T>} Ok
*/
/**
* @template T
* @typedef ErrMethods
* @property {typeof isOk} isOk
* @property {typeof isErr} isErr
* @property {typeof chain} chain
* @property {typeof map} map
* @property {typeof alt} alt
* @property {typeof fold} fold
* @property {typeof bimap} bimap
*/
/**
* @template T
* @typedef {Set<T> & Construct<T> & ErrMethods<T>} Err
*/
/**
* @template T, U, E
* @this {Result<T, E>}
* @param {Morphism<T, U>} fn
* @returns {Result<U, E>}
*/
function map(fn) {
return /** @type {Result<U, E>} */ (this[Self](this.chain(fn)))
}
/**
* @template T1, T2, E1, E2
* @this {Result<T1, E1>}
* @param {Morphism<T1, T2>} y
* @param {Morphism<E1, E2>} x
* @returns {Result<T2, E2>}
*/
function bimap(x, y) {
return map.call(this, y).map(x)
}
/**
* @template T, U, E
* @this {Result<T, E>}
* @param {Morphism<T, U>} fn
* @param {U} acc
* @return {U}
*/
function fold(fn, acc) {
const result = this.map(fn)
return result.isOk() ? result[Value] : acc
}
/**
* @template T, E
* @this {Result<T, E>}
* @param {Result<T, E>} other
* @returns {Result<T, E>}
*/
function alt(other) {
return this.isOk() ? this : other
}
/**
* @template T, E
* @this {Result<T, E>}
* @returns {this is Ok<T>}
*/
function isOk() {
return this[Self] === Ok
}
/**
* @template T, E
* @this {Result<T, E>}
* @returns {this is Err<E>}
*/
function isErr() {
return this[Self] === Err
}
/**
* @template T
* @param {?T} value
* @returns {Ok<T>}
*/
export const Ok = value => Object.freeze({
[Self]: Ok,
[Value]: value,
isOk,
isErr,
chain,
map,
alt,
fold,
bimap
})
/**
* @template T
* @param {T?} value
* @returns {Err<T>}
*/
export const Err = value => Object.freeze({
[Self]: Err,
[Value]: value,
chain: constant,
map: constant,
alt,
isOk,
isErr,
fold,
bimap
})
/**
* @template T, E
* @param {T} value * @param {T} value
* @returns {Result<T, E>} * @constructs {Ok<T, E>}
*/ */
export const Result = value => Ok(value) constructor(value) {
Result.of = Result super()
Result.zero = () => Err(undefined) this.#value = value
}
/** @returns {this is Ok<T, E>} */
isOk() { return true }
/** @returns {this is Err<T, E>} */
isErr() { return false }
/**
* @template R
* @param {Morphism<T, R>} f
* @this {Ok<T, E>}
* @returns {R}
*/
chain(f) {
return f(this.#value)
}
/**
* @template R
* @param {Morphism<T, R>} f
* @this {Ok<T, E>}
* @returns {Ok<R, E>}
*/
map(f) {
return Result.of(this.chain(f))
}
/**
* @template R
* @param {Morphism<T, R>} f
* @this {Ok<T, E>}
* @returns {Ok<R, E>}
*/
then(f) {
return this.map(f)
}
/**
* @template R
* @param {Morphism<E, R>} _f
* @returns {this}
*/
catch(_f) {
return this
}
/**
* @template U
* @param {(acc: U, value: T) => U} f
* @param {U} init
* @returns {U}
*/
reduce(f, init) {
return f(init, this.#value)
}
}
/**
* @template T, E
*/
export class Err extends Algebra(Functor, Monad) {
/** @type {E} */
#value
/**
* @param {E} value
* @constructs {Err<T, E>}
*/
constructor(value) {
super()
this.#value = value
}
/** @returns {this is Ok<T, E>} */
isOk() { return false }
/** @returns {this is Err<T, E>} */
isErr() { return true }
/**
* @template R
* @param {Morphism<T, R>} _f
* @returns {this}
*/
chain(_f) {
return this
}
/**
* @template R
* @param {Morphism<T, R>} _f
* @returns {this}
*/
map(_f) {
return this
}
/**
* @template R
* @param {Morphism<T, R>} _f
* @returns {this}
*/
then(_f) {
return this
}
/**
* @template R
* @param {Morphism<E, R>} f
* @returns {Err<T, R>}
*/
catch(f) {
return new Err(f(this.#value))
}
/**
* @template U
* @param {(acc: U, value: T) => U} _f
* @param {U} init
* @returns {U}
*/
reduce(_f, init) {
return init
}
}
/**
* @template T, E
* @type {Ok<T, E> | Err<T, E>} Result
*/
export class Result {
/**
* @template T
* @param {T} value
* @returns {Ok}
*/
static of(value) {
return new Ok(value)
}
}
/**
* @template T, E
* @param {T} v
* @returns {Ok<T, E>}
*/
export const ok = v => new Ok(v)
/**
* @template T, E
* @param {E} e
* @returns {Err<T, E>}
*/
export const err = e => new Err(e)

33
src/algebra/types.js Normal file
View file

@ -0,0 +1,33 @@
export default {}
/**
* @template T, R
* @typedef {(value: T) => R} Morphism
*/
/**
* @template T
* @typedef {<R>(value: T) => R} InferredMorphism
*/
/**
* @template T
* @typedef {(value: T) => boolean} Predicate
*/
/**
* @template T
* @typedef {(value: T) => Chain<T>} ChainConstructor
*/
/**
* @template T
* @typedef {<R>(f: Morphism<T, R>) => R} chain
*/
/**
* @template T
* @typedef {{ chain: chain<T> }} Chain
*/
/** @typedef {(...args: any[]) => any} Fn */

256
src/mixin.js Normal file
View file

@ -0,0 +1,256 @@
/**
* mixwith.js
* Author: Justin Fagnani (https://github.com/justinfagnani/)
* https://github.com/justinfagnani/mixwith.js
*/
'use strict'
// used by apply() and isApplicationOf()
const _appliedMixin = '__mixwith_appliedMixin'
/**
* A function that returns a subclass of its argument.
*
* @example
* const M = (superclass) => class extends superclass {
* getMessage() {
* return "Hello"
* }
* }
*
* @typedef {Function} MixinFunction
* @param {Function} superclass
* @return {Function} A subclass of `superclass`
*/
/**
* Applies `mixin` to `superclass`.
*
* `apply` stores a reference from the mixin application to the unwrapped mixin
* to make `isApplicationOf` and `hasMixin` work.
*
* This function is usefull for mixin wrappers that want to automatically enable
* {@link hasMixin} support.
*
* @example
* const Applier = (mixin) => wrap(mixin, (superclass) => apply(superclass, mixin))
*
* // M now works with `hasMixin` and `isApplicationOf`
* const M = Applier((superclass) => class extends superclass {})
*
* class C extends M(Object) {}
* let i = new C()
* hasMixin(i, M) // true
*
* @function
* @param {Function} superclass A class or constructor function
* @param {MixinFunction} mixin The mixin to apply
* @return {Function} A subclass of `superclass` produced by `mixin`
*/
export const apply = (superclass, mixin) => {
let application = mixin(superclass)
application.prototype[_appliedMixin] = unwrap(mixin)
return application
}
/**
* Returns `true` iff `proto` is a prototype created by the application of
* `mixin` to a superclass.
*
* `isApplicationOf` works by checking that `proto` has a reference to `mixin`
* as created by `apply`.
*
* @function
* @param {Object} proto A prototype object created by {@link apply}.
* @param {MixinFunction} mixin A mixin function used with {@link apply}.
* @return {boolean} whether `proto` is a prototype created by the application of
* `mixin` to a superclass
*/
export const isApplicationOf = (proto, mixin) =>
proto.hasOwnProperty(_appliedMixin) && proto[_appliedMixin] === unwrap(mixin)
/**
* Returns `true` iff `o` has an application of `mixin` on its prototype
* chain.
*
* @function
* @param {Object} o An object
* @param {MixinFunction} mixin A mixin applied with {@link apply}
* @return {boolean} whether `o` has an application of `mixin` on its prototype
* chain
*/
export const hasMixin = (o, mixin) => {
while (o != null) {
if (isApplicationOf(o, mixin)) return true
o = Object.getPrototypeOf(o)
}
return false
}
// used by wrap() and unwrap()
const _wrappedMixin = '__mixwith_wrappedMixin'
/**
* Sets up the function `mixin` to be wrapped by the function `wrapper`, while
* allowing properties on `mixin` to be available via `wrapper`, and allowing
* `wrapper` to be unwrapped to get to the original function.
*
* `wrap` does two things:
* 1. Sets the prototype of `mixin` to `wrapper` so that properties set on
* `mixin` inherited by `wrapper`.
* 2. Sets a special property on `mixin` that points back to `mixin` so that
* it can be retreived from `wrapper`
*
* @function
* @param {MixinFunction} mixin A mixin function
* @param {MixinFunction} wrapper A function that wraps {@link mixin}
* @return {MixinFunction} `wrapper`
*/
export const wrap = (mixin, wrapper) => {
Object.setPrototypeOf(wrapper, mixin)
if (!mixin[_wrappedMixin]) {
mixin[_wrappedMixin] = mixin
}
return wrapper
}
/**
* Unwraps the function `wrapper` to return the original function wrapped by
* one or more calls to `wrap`. Returns `wrapper` if it's not a wrapped
* function.
*
* @function
* @param {MixinFunction} wrapper A wrapped mixin produced by {@link wrap}
* @return {MixinFunction} The originally wrapped mixin
*/
export const unwrap = (wrapper) => wrapper[_wrappedMixin] || wrapper
const _cachedApplications = '__mixwith_cachedApplications'
/**
* Decorates `mixin` so that it caches its applications. When applied multiple
* times to the same superclass, `mixin` will only create one subclass, memoize
* it and return it for each application.
*
* Note: If `mixin` somehow stores properties its classes constructor (static
* properties), or on its classes prototype, it will be shared across all
* applications of `mixin` to a super class. It's reccomended that `mixin` only
* access instance state.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap with caching behavior
* @return {MixinFunction} a new mixin function
*/
export const Cached = (mixin) => wrap(mixin, (superclass) => {
// Get or create a symbol used to look up a previous application of mixin
// to the class. This symbol is unique per mixin definition, so a class will have N
// applicationRefs if it has had N mixins applied to it. A mixin will have
// exactly one _cachedApplicationRef used to store its applications.
let cachedApplications = superclass[_cachedApplications]
if (!cachedApplications) {
cachedApplications = superclass[_cachedApplications] = new Map()
}
let application = cachedApplications.get(mixin)
if (!application) {
application = mixin(superclass)
cachedApplications.set(mixin, application)
}
return application
})
/**
* Decorates `mixin` so that it only applies if it's not already on the
* prototype chain.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap with deduplication behavior
* @return {MixinFunction} a new mixin function
*/
export const DeDupe = (mixin) => wrap(mixin, (superclass) =>
(hasMixin(superclass.prototype, mixin))
? superclass
: mixin(superclass))
/**
* Adds [Symbol.hasInstance] (ES2015 custom instanceof support) to `mixin`.
*
* @function
* @param {MixinFunction} mixin The mixin to add [Symbol.hasInstance] to
* @return {MixinFunction} the given mixin function
*/
export const HasInstance = (mixin) => {
if (Symbol && Symbol.hasInstance && !mixin[Symbol.hasInstance]) {
Object.defineProperty(mixin, Symbol.hasInstance, {
value(o) {
return hasMixin(o, mixin)
},
})
}
return mixin
}
/**
* A basic mixin decorator that applies the mixin with {@link apply} so that it
* can be used with {@link isApplicationOf}, {@link hasMixin} and the other
* mixin decorator functions.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap
* @return {MixinFunction} a new mixin function
*/
export const BareMixin = (mixin) => wrap(mixin, (s) => apply(s, mixin))
/**
* Decorates a mixin function to add deduplication, application caching and
* instanceof support.
*
* @function
* @param {MixinFunction} mixin The mixin to wrap
* @return {MixinFunction} a new mixin function
*/
export const Mixin = (mixin) => DeDupe(Cached(BareMixin(mixin)))
/**
* A fluent interface to apply a list of mixins to a superclass.
*
* ```javascript
* class X extends mix(Object).with(A, B, C) {}
* ```
*
* The mixins are applied in order to the superclass, so the prototype chain
* will be: X->C'->B'->A'->Object.
*
* This is purely a convenience function. The above example is equivalent to:
*
* ```javascript
* class X extends C(B(A(Object))) {}
* ```
*
* @function
* @param {Function} [superclass=Object]
* @return {MixinBuilder}
*/
export const mix = (superclass) => new MixinBuilder(superclass)
class MixinBuilder {
constructor(superclass) {
this.superclass = superclass || class { }
}
/**
* Applies `mixins` in order to the superclass given to `mix()`.
*
* @param {Array.<Mixin>} mixins
* @return {FunctionConstructor} a subclass of `superclass` with `mixins` applied
*/
with(...mixins) {
return mixins.reduce((c, m) => m(c), this.superclass)
}
}

View file

@ -10,7 +10,7 @@
/** /**
* @template R * @template R
* @callback match * @callback cata
* @param {Record<string, Fn<R>> & Partial<Record<'_', EmptyFn<R>>>} pattern * @param {Record<string, Fn<R>> & Partial<Record<'_', EmptyFn<R>>>} pattern
* @returns {R} * @returns {R}
* *
@ -19,7 +19,7 @@
/** /**
* @typedef Variant * @typedef Variant
* @property {match<any>} match * @property {cata<any>} cata
*/ */
/** /**
@ -39,21 +39,21 @@
const Tag = Symbol('Tag') const Tag = Symbol('Tag')
class MatchError extends Error { export class CatamorphismError extends Error {
/** @param {PropertyKey} name */ /** @param {PropertyKey} name */
constructor(name) { constructor(name) {
super(`unmatched arm in match: ${name.toString()}`) super(`unmatched arm in catamorphism: ${name.toString()}`)
} }
} }
/** /**
* @param {PropertyKey} variant * @param {PropertyKey} type
* @param {PropertyKey} variant * @param {PropertyKey} variant
* @returns {(...values: any[]) => Variant} * @returns {(...values: any[]) => Variant}
*/ */
const Variant = (type, variant) => (...values) => ({ const Variant = (type, variant) => (...values) => ({
[Tag]: type, [Tag]: type,
match(pattern) { cata(pattern) {
if (variant in pattern) { if (variant in pattern) {
// NOTE: this is a workaround for typescript not recognizing // NOTE: this is a workaround for typescript not recognizing
// that objects can be indexed with symbols // that objects can be indexed with symbols
@ -61,7 +61,7 @@ const Variant = (type, variant) => (...values) => ({
} else if ('_' in pattern) { } else if ('_' in pattern) {
return pattern._() return pattern._()
} else { } else {
throw new MatchError(variant) throw new CatamorphismError(variant)
} }
} }
}) })