/** * This module provides an implementation of the `Equivalence` type class, which defines a binary relation * that is reflexive, symmetric, and transitive. In other words, it defines a notion of equivalence between values of a certain type. * These properties are also known in mathematics as an "equivalence relation". * * @since 2.0.0 */ import { dual } from "./Function.js" import type { TypeLambda } from "./HKT.js" /** * @category type class * @since 2.0.0 */ export interface Equivalence { (self: A, that: A): boolean } /** * @category type lambdas * @since 2.0.0 */ export interface EquivalenceTypeLambda extends TypeLambda { readonly type: Equivalence } /** * @category constructors * @since 2.0.0 */ export const make = (isEquivalent: (self: A, that: A) => boolean): Equivalence => (self: A, that: A): boolean => self === that || isEquivalent(self, that) const isStrictEquivalent = (x: unknown, y: unknown) => x === y /** * Return an `Equivalence` that uses strict equality (===) to compare values. * * @since 2.0.0 * @category constructors */ export const strict: () => Equivalence = () => isStrictEquivalent /** * @category instances * @since 2.0.0 */ export const string: Equivalence = strict() /** * @category instances * @since 2.0.0 */ export const number: Equivalence = strict() /** * @category instances * @since 2.0.0 */ export const boolean: Equivalence = strict() /** * @category instances * @since 2.0.0 */ export const bigint: Equivalence = strict() /** * @category instances * @since 2.0.0 */ export const symbol: Equivalence = strict() /** * @category combining * @since 2.0.0 */ export const combine: { /** * @category combining * @since 2.0.0 */ (that: Equivalence): (self: Equivalence) => Equivalence /** * @category combining * @since 2.0.0 */ (self: Equivalence, that: Equivalence): Equivalence } = dual(2, (self: Equivalence, that: Equivalence): Equivalence => make((x, y) => self(x, y) && that(x, y))) /** * @category combining * @since 2.0.0 */ export const combineMany: { /** * @category combining * @since 2.0.0 */ (collection: Iterable>): (self: Equivalence) => Equivalence /** * @category combining * @since 2.0.0 */ (self: Equivalence, collection: Iterable>): Equivalence } = dual(2, (self: Equivalence, collection: Iterable>): Equivalence => make((x, y) => { if (!self(x, y)) { return false } for (const equivalence of collection) { if (!equivalence(x, y)) { return false } } return true })) const isAlwaysEquivalent: Equivalence = (_x, _y) => true /** * @category combining * @since 2.0.0 */ export const combineAll = (collection: Iterable>): Equivalence => combineMany(isAlwaysEquivalent, collection) /** * @category mapping * @since 2.0.0 */ export const mapInput: { /** * @category mapping * @since 2.0.0 */ (f: (b: B) => A): (self: Equivalence) => Equivalence /** * @category mapping * @since 2.0.0 */ (self: Equivalence, f: (b: B) => A): Equivalence } = dual( 2, (self: Equivalence, f: (b: B) => A): Equivalence => make((x, y) => self(f(x), f(y))) ) /** * @category instances * @since 2.0.0 */ export const Date: Equivalence = mapInput(number, (date) => date.getTime()) /** * @category combining * @since 2.0.0 */ export const product: { (that: Equivalence): (self: Equivalence) => Equivalence // readonly because invariant (self: Equivalence, that: Equivalence): Equivalence // readonly because invariant } = dual( 2, (self: Equivalence, that: Equivalence): Equivalence => make(([xa, xb], [ya, yb]) => self(xa, ya) && that(xb, yb)) ) /** * @category combining * @since 2.0.0 */ export const all = (collection: Iterable>): Equivalence> => { return make((x, y) => { const len = Math.min(x.length, y.length) let collectionLength = 0 for (const equivalence of collection) { if (collectionLength >= len) { break } if (!equivalence(x[collectionLength], y[collectionLength])) { return false } collectionLength++ } return true }) } /** * @category combining * @since 2.0.0 */ export const productMany = ( self: Equivalence, collection: Iterable> ): Equivalence]> /* readonly because invariant */ => { const equivalence = all(collection) return make((x, y) => !self(x[0], y[0]) ? false : equivalence(x.slice(1), y.slice(1))) } /** * Similar to `Promise.all` but operates on `Equivalence`s. * * ```ts skip-type-checking * [Equivalence, Equivalence, ...] -> Equivalence<[A, B, ...]> * ``` * * Given a tuple of `Equivalence`s returns a new `Equivalence` that compares values of a tuple * by applying each `Equivalence` to the corresponding element of the tuple. * * @category combinators * @since 2.0.0 */ export const tuple = >>( ...elements: T ): Equivalence] ? A : never }>> => all(elements) as any /** * Creates a new `Equivalence` for an array of values based on a given `Equivalence` for the elements of the array. * * @category combinators * @since 2.0.0 */ export const array = (item: Equivalence): Equivalence> => make((self, that) => { if (self.length !== that.length) { return false } for (let i = 0; i < self.length; i++) { const isEq = item(self[i], that[i]) if (!isEq) { return false } } return true }) /** * Given a struct of `Equivalence`s returns a new `Equivalence` that compares values of a struct * by applying each `Equivalence` to the corresponding property of the struct. * * @category combinators * @since 2.0.0 */ export const struct = >>( fields: R ): Equivalence<{ readonly [K in keyof R]: [R[K]] extends [Equivalence] ? A : never }> => { const keys = Object.keys(fields) return make((self, that) => { for (const key of keys) { if (!fields[key](self[key], that[key])) { return false } } return true }) }