94 lines
2 KiB
JavaScript
94 lines
2 KiB
JavaScript
/**
|
|
* @template R
|
|
* @typedef {(...args: any[]) => R} Fn
|
|
*/
|
|
|
|
/**
|
|
* @template R
|
|
* @typedef {() => R} EmptyFn
|
|
*/
|
|
|
|
/**
|
|
* @template R
|
|
* @callback cata
|
|
* @param {Record<string, Fn<R>> & Partial<Record<'_', EmptyFn<R>>>} pattern
|
|
* @returns {R}
|
|
*
|
|
* @throws MatchError
|
|
*/
|
|
|
|
/**
|
|
* @typedef Variant
|
|
* @property {cata<any>} cata
|
|
*/
|
|
|
|
/**
|
|
* @typedef {(...values: any[]) => Variant} VariantConstructor
|
|
*/
|
|
|
|
/**
|
|
* @template {PropertyKey[]} Variant
|
|
* @typedef {{ [key in Variant[number]]: (...values: any) => Variant }} Variants
|
|
*/
|
|
|
|
/**
|
|
* @template {PropertyKey} T
|
|
* @template {PropertyKey[]} U
|
|
* @typedef {{ is: typeof is } & Variants<U>} Union
|
|
*/
|
|
|
|
const Tag = Symbol('Tag')
|
|
|
|
export class CatamorphismError extends Error {
|
|
/** @param {PropertyKey} name */
|
|
constructor(name) {
|
|
super(`unmatched arm in catamorphism: ${name.toString()}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {PropertyKey} type
|
|
* @param {PropertyKey} variant
|
|
* @returns {(...values: any[]) => Variant}
|
|
*/
|
|
const Variant = (type, variant) => (...values) => ({
|
|
[Tag]: type,
|
|
cata(pattern) {
|
|
if (variant in pattern) {
|
|
// NOTE: this is a workaround for typescript not recognizing
|
|
// that objects can be indexed with symbols
|
|
return pattern[ /** @type {string} */ (variant)](...values)
|
|
} else if ('_' in pattern) {
|
|
return pattern._()
|
|
} else {
|
|
throw new CatamorphismError(variant)
|
|
}
|
|
}
|
|
})
|
|
|
|
/**
|
|
* @template T, U
|
|
* @this {Union<T, U>}
|
|
* @param {any} other
|
|
* @returns {other is Variant<U>}
|
|
*/
|
|
function is(other) {
|
|
return Object.hasOwn(other, Tag) && other[Tag] === this[Tag]
|
|
}
|
|
|
|
/**
|
|
* @template {PropertyKey} const T
|
|
* @template {Array<PropertyKey>} const U
|
|
* @param {T} typeName
|
|
* @param {...U} variantNames
|
|
* @returns {Union<T, U>}
|
|
*/
|
|
export const Union = (typeName, variantNames) => {
|
|
const tag = { [Tag]: typeName, is }
|
|
const variants = Object.fromEntries(variantNames.map(v => [v, Variant(typeName, v)]))
|
|
const result = Object.assign(tag, variants)
|
|
|
|
|
|
return /** @type {Union<T, U>} */ (result)
|
|
}
|
|
|