/** * This file contains the Datum algebraic data type. Datum represents an * optional value that has an additional notation for a pending state. * * @module Datum * @since 2.0.0 */ import type { $, Kind, Out } from "./kind.ts"; import type { Applicable } from "./applicable.ts"; import type { Combinable } from "./combinable.ts"; import type { Comparable } from "./comparable.ts"; import type { Either } from "./either.ts"; import type { Filterable } from "./filterable.ts"; import type { Bind, Flatmappable, Tap } from "./flatmappable.ts"; import type { Initializable } from "./initializable.ts"; import type { BindTo, Mappable } from "./mappable.ts"; import type { Option } from "./option.ts"; import type { Pair } from "./pair.ts"; import type { Predicate } from "./predicate.ts"; import type { Refinement } from "./refinement.ts"; import type { Showable } from "./showable.ts"; import type { Sortable } from "./sortable.ts"; import type { Traversable } from "./traversable.ts"; import type { Wrappable } from "./wrappable.ts"; import * as O from "./option.ts"; import { createBind, createTap } from "./flatmappable.ts"; import { createBindTo } from "./mappable.ts"; import { fromSort } from "./sortable.ts"; import { isNotNil } from "./nil.ts"; import { flow, handleThrow, identity, pipe } from "./fn.ts"; /** * The Initial state represents a datum that has not been loaded yet. * * @since 2.0.0 */ export type Initial = { readonly tag: "Initial"; }; /** * The Pending state represents a datum that is currently loading. * * @since 2.0.0 */ export type Pending = { readonly tag: "Pending"; }; /** * The Refresh state represents a datum that is refreshing with stale data. * * @since 2.0.0 */ export type Refresh = { readonly tag: "Refresh"; readonly value: A; }; /** * The Replete state represents a datum that has been fully loaded. * * @since 2.0.0 */ export type Replete = { readonly tag: "Replete"; readonly value: A; }; /** * The Datum type represents an optional value with loading states. * * @since 2.0.0 */ export type Datum = Initial | Pending | Refresh | Replete; /** * The None type represents states without a value. * * @since 2.0.0 */ export type None = Initial | Pending; /** * The Some type represents states with a value. * * @since 2.0.0 */ export type Some = Refresh | Replete; /** * The Loading type represents states that are currently loading. * * @since 2.0.0 */ export type Loading = Pending | Refresh; /** * Specifies Datum as a Higher Kinded Type, with covariant * parameter A corresponding to the 0th index of any substitutions. * * @since 2.0.0 */ export interface KindDatum extends Kind { readonly kind: Datum>; } /** * Create an Initial datum. * * @example * ```ts * import { initial } from "./datum.ts"; * * const datum = initial; * console.log(datum); // { tag: "Initial" } * ``` * * @since 2.0.0 */ export const initial: Initial = { tag: "Initial" }; /** * Create a Pending datum. * * @example * ```ts * import { pending } from "./datum.ts"; * * const datum = pending; * console.log(datum); // { tag: "Pending" } * ``` * * @since 2.0.0 */ export const pending: Pending = { tag: "Pending" }; /** * Create a Refresh datum with a value. * * @example * ```ts * import { refresh } from "./datum.ts"; * * const datum = refresh("stale data"); * console.log(datum); // { tag: "Refresh", value: "stale data" } * ``` * * @since 2.0.0 */ export function refresh(value: D): Datum { return ({ tag: "Refresh", value }); } /** * Create a Replete datum with a value. * * @example * ```ts * import { replete } from "./datum.ts"; * * const datum = replete("fresh data"); * console.log(datum); // { tag: "Replete", value: "fresh data" } * ``` * * @since 2.0.0 */ export function replete(value: D): Datum { return ({ tag: "Replete", value }); } /** * Create a constant Initial datum. * * @example * ```ts * import { constInitial } from "./datum.ts"; * * const datum = constInitial(); * console.log(datum); // { tag: "Initial" } * ``` * * @since 2.0.0 */ export function constInitial(): Datum { return initial; } /** * Create a constant Pending datum. * * @example * ```ts * import { constPending } from "./datum.ts"; * * const datum = constPending(); * console.log(datum); // { tag: "Pending" } * ``` * * @since 2.0.0 */ export function constPending(): Datum { return pending; } /** * Create a Datum from a nullable value. * * @example * ```ts * import { fromNullable } from "./datum.ts"; * * const datum1 = fromNullable("value"); // Replete("value") * const datum2 = fromNullable(null); // Initial * ``` * * @since 2.0.0 */ export function fromNullable(a: A): Datum> { return isNotNil(a) ? replete(a) : initial; } /** * Create a Datum from a function that might throw. * * @example * ```ts * import { tryCatch } from "./datum.ts"; * * const safeParse = tryCatch(JSON.parse); * const result1 = safeParse('{"key": "value"}'); // Replete({key: "value"}) * const result2 = safeParse('invalid json'); // Initial * ``` * * @since 2.0.0 */ export function tryCatch( fasr: (...as: AS) => A, ): (...as: AS) => Datum { return handleThrow( fasr, replete, constInitial, ); } /** * Convert a Datum to a Loading state. * * @example * ```ts * import { toLoading, replete, initial } from "./datum.ts"; * * const datum1 = toLoading(replete("data")); // Refresh("data") * const datum2 = toLoading(initial); // Pending * ``` * * @since 2.0.0 */ export function toLoading(ta: Datum): Datum { return pipe( ta, match( constPending, constPending, refresh, refresh, ), ); } /** * Check if a Datum is in the Initial state. * * @example * ```ts * import { isInitial, initial, pending, replete } from "./datum.ts"; * * console.log(isInitial(initial)); // true * console.log(isInitial(pending)); // false * console.log(isInitial(replete("data"))); // false * ``` * * @since 2.0.0 */ export function isInitial(ta: Datum): ta is Initial { return ta.tag === "Initial"; } /** * Check if a Datum is in the Pending state. * * @example * ```ts * import { isPending, initial, pending, replete } from "./datum.ts"; * * console.log(isPending(initial)); // false * console.log(isPending(pending)); // true * console.log(isPending(replete("data"))); // false * ``` * * @since 2.0.0 */ export function isPending(ta: Datum): ta is Pending { return ta.tag === "Pending"; } /** * Check if a Datum is in the Refresh state. * * @example * ```ts * import { isRefresh, initial, refresh, replete } from "./datum.ts"; * * console.log(isRefresh(initial)); // false * console.log(isRefresh(refresh("data"))); // true * console.log(isRefresh(replete("data"))); // false * ``` * * @since 2.0.0 */ export function isRefresh(ta: Datum): ta is Refresh { return ta.tag === "Refresh"; } /** * Check if a Datum is in the Replete state. * * @example * ```ts * import { isReplete, initial, refresh, replete } from "./datum.ts"; * * console.log(isReplete(initial)); // false * console.log(isReplete(refresh("data"))); // false * console.log(isReplete(replete("data"))); // true * ``` * * @since 2.0.0 */ export function isReplete(ta: Datum): ta is Replete { return ta.tag === "Replete"; } /** * Check if a Datum has no value (Initial or Pending). * * @example * ```ts * import { isNone, initial, pending, replete } from "./datum.ts"; * * console.log(isNone(initial)); // true * console.log(isNone(pending)); // true * console.log(isNone(replete("data"))); // false * ``` * * @since 2.0.0 */ export function isNone(ta: Datum): ta is None { return isInitial(ta) || isPending(ta); } /** * Check if a Datum has a value (Refresh or Replete). * * @example * ```ts * import { isSome, initial, refresh, replete } from "./datum.ts"; * * console.log(isSome(initial)); // false * console.log(isSome(refresh("data"))); // true * console.log(isSome(replete("data"))); // true * ``` * * @since 2.0.0 */ export function isSome(ta: Datum): ta is Some { return isRefresh(ta) || isReplete(ta); } /** * Check if a Datum is loading (Pending or Refresh). * * @example * ```ts * import { isLoading, initial, pending, refresh, replete } from "./datum.ts"; * * console.log(isLoading(initial)); // false * console.log(isLoading(pending)); // true * console.log(isLoading(refresh("data"))); // true * console.log(isLoading(replete("data"))); // false * ``` * * @since 2.0.0 */ export function isLoading(ta: Datum): ta is Loading { return isPending(ta) || isRefresh(ta); } /** * Pattern match on a Datum to extract values. * * @example * ```ts * import { match, initial, pending, refresh, replete } from "./datum.ts"; * * const matcher = match( * () => "Not started", * () => "Loading...", * (value) => `Fresh: ${value}`, * (value) => `Stale: ${value}` * ); * * console.log(matcher(initial)); // "Not started" * console.log(matcher(pending)); // "Loading..." * console.log(matcher(refresh("data"))); // "Stale: data" * console.log(matcher(replete("data"))); // "Fresh: data" * ``` * * @since 2.0.0 */ export function match( onInitial: () => B, onPending: () => B, onReplete: (a: A) => B, onRefresh: (a: A) => B, ): (ua: Datum) => B { return (ua) => { switch (ua.tag) { case "Initial": return onInitial(); case "Pending": return onPending(); case "Refresh": return onRefresh(ua.value); case "Replete": return onReplete(ua.value); } }; } /** * Get the value from a Datum or return a default. * * @example * ```ts * import { getOrElse, initial, replete } from "./datum.ts"; * * const getValue = getOrElse(() => "default"); * console.log(getValue(initial)); // "default" * console.log(getValue(replete("data"))); // "data" * ``` * * @since 2.0.0 */ export function getOrElse(onNone: () => A): (ua: Datum) => A { return match(onNone, onNone, identity, identity); } /** * Wrap a value in a Replete Datum. * * @example * ```ts * import { wrap } from "./datum.ts"; * * const datum = wrap(42); * console.log(datum); // { tag: "Replete", value: 42 } * ``` * * @since 2.0.0 */ export function wrap(a: A): Datum { return replete(a); } /** * Apply a function to the value in a Datum. * * @example * ```ts * import { map, replete, initial } from "./datum.ts"; * import { pipe } from "./fn.ts"; * * const datum = pipe( * replete(5), * map(n => n * 2) * ); * console.log(datum); // { tag: "Replete", value: 10 } * ``` * * @since 2.0.0 */ export function map(fai: (a: A) => I): (ta: Datum) => Datum { return match( constInitial, constPending, flow(fai, replete), flow(fai, refresh), ); } /** * Apply a function wrapped in a Datum to a value wrapped in a Datum. * * @example * ```ts * import { apply, replete, initial } from "./datum.ts"; * import { pipe } from "./fn.ts"; * * const datumFn = replete((n: number) => n * 2); * const datumValue = replete(5); * const result = pipe( * datumFn, * apply(datumValue) * ); * console.log(result); // { tag: "Replete", value: 10 } * ``` * * @since 2.0.0 */ export function apply( ua: Datum, ): (ufai: Datum<(a: A) => I>) => Datum { switch (ua.tag) { case "Initial": return (ufai) => isLoading(ufai) ? pending : initial; case "Pending": return constPending; case "Replete": return (ufai) => isReplete(ufai) ? replete(ufai.value(ua.value)) : isRefresh(ufai) ? refresh(ufai.value(ua.value)) : isLoading(ufai) ? pending : initial; case "Refresh": return (ufai) => isSome(ufai) ? refresh(ufai.value(ua.value)) : pending; } } /** * Chain Datum computations together. * * @example * ```ts * import { flatmap, replete, initial } from "./datum.ts"; * import { pipe } from "./fn.ts"; * * const datum = pipe( * replete(5), * flatmap(n => replete(n * 2)) * ); * console.log(datum); // { tag: "Replete", value: 10 } * ``` * * @since 2.0.0 */ export function flatmap( fati: (a: A) => Datum, ): (ta: Datum) => Datum { return match( constInitial, constPending, fati, flow(fati, toLoading), ); } /** * Provide an alternative Datum if the current one has no value. * * @example * ```ts * import { alt, initial, replete } from "./datum.ts"; * import { pipe } from "./fn.ts"; * * const result1 = pipe(initial, alt(replete("fallback"))); // Replete("fallback") * const result2 = pipe(replete("data"), alt(replete("fallback"))); // Replete("data") * ``` * * @since 2.0.0 */ export function alt(tb: Datum): (ta: Datum) => Datum { return (ta) => isSome(ta) ? ta : tb; } /** * Fold over a Datum to produce a single value. * * @example * ```ts * import { fold, initial, replete } from "./datum.ts"; * * const folder = fold( * (acc: number, value: number) => acc + value, * 0 * ); * * console.log(folder(initial)); // 0 * console.log(folder(replete(5))); // 5 * ``` * * @since 2.0.0 */ export function fold( foao: (o: O, a: A) => O, o: O, ): (ta: Datum) => O { return (ta) => isSome(ta) ? foao(o, ta.value) : o; } /** * Check if a Datum satisfies a predicate. * * @example * ```ts * import { exists, replete, initial } from "./datum.ts"; * * const isEven = (n: number) => n % 2 === 0; * console.log(exists(isEven)(replete(4))); // true * console.log(exists(isEven)(replete(3))); // false * console.log(exists(isEven)(initial)); // false * ``` * * @since 2.0.0 */ export function exists(predicate: Predicate): (ua: Datum) => boolean { return (ua) => isSome(ua) && predicate(ua.value); } /** * Filter a Datum based on a predicate. * * @example * ```ts * import { filter, replete, initial } from "./datum.ts"; * * const isEven = (n: number) => n % 2 === 0; * console.log(filter(isEven)(replete(4))); // Replete(4) * console.log(filter(isEven)(replete(3))); // Initial * console.log(filter(isEven)(initial)); // Initial * ``` * * @since 2.0.0 */ export function filter( refinement: Refinement, ): (ta: Datum) => Datum; export function filter( predicate: Predicate, ): (ta: Datum) => Datum; export function filter( predicate: Predicate, ): (ta: Datum) => Datum { const _exists = exists(predicate); return (ta) => _exists(ta) ? ta : isLoading(ta) ? pending : initial; } /** * Filter and map a Datum using an Option. * * @example * ```ts * import { filterMap, replete, initial } from "./datum.ts"; * import * as O from "./option.ts"; * * const safeDivide = (n: number) => n === 0 ? O.none : O.some(10 / n); * console.log(filterMap(safeDivide)(replete(5))); // Replete(2) * console.log(filterMap(safeDivide)(replete(0))); // Initial * ``` * * @since 2.0.0 */ export function filterMap( fai: (a: A) => Option, ): (ua: Datum) => Datum { return (ua) => { if (isNone(ua)) { return ua; } const oi = fai(ua.value); if (isReplete(ua)) { return O.isNone(oi) ? initial : replete(oi.value); } else { return O.isNone(oi) ? pending : refresh(oi.value); } }; } /** * Partition a Datum based on a predicate. * * @example * ```ts * import { partition, replete, initial } from "./datum.ts"; * * const isEven = (n: number) => n % 2 === 0; * const [evens, odds] = partition(isEven)(replete(4)); * console.log(evens); // Replete(4) * console.log(odds); // Initial * ``` * * @since 2.0.0 */ export function partition( refinement: Refinement, ): (ua: Datum) => Pair, Datum>; export function partition( predicate: Predicate, ): (ua: Datum) => Pair, Datum>; export function partition( predicate: Predicate, ): (ua: Datum) => Pair, Datum> { return (ua) => { if (isNone(ua)) { return [ua, ua]; } if (predicate(ua.value)) { if (isReplete(ua)) { return [ua, initial]; } return [ua, pending]; } if (isReplete(ua)) { return [initial, ua]; } return [pending, ua]; }; } /** * Partition and map a Datum using an Either. * * @example * ```ts * import { partitionMap, replete, initial } from "./datum.ts"; * import * as E from "./either.ts"; * * const classify = (n: number) => n % 2 === 0 ? E.right(n) : E.left(n); * const [evens, odds] = partitionMap(classify)(replete(4)); * console.log(evens); // Replete(4) * console.log(odds); // Initial * ``` * * @since 2.0.0 */ export function partitionMap( fai: (a: A) => Either, ): (ua: Datum) => Pair, Datum> { return (ua) => { if (isNone(ua)) { return [ua, ua]; } const result = fai(ua.value); if (isReplete(ua)) { return result.tag === "Right" ? [replete(result.right), initial] : [initial, replete(result.left)]; } else { return result.tag === "Right" ? [refresh(result.right), pending] : [pending, refresh(result.left)]; } }; } /** * Traverse over a Datum using the supplied Applicable. * * @example * ```ts * import { traverse, replete } from "./datum.ts"; * import * as O from "./option.ts"; * import { pipe } from "./fn.ts"; * * const datum = pipe( * replete(5), * traverse(O.ApplicableOption)(n => O.some(n * 2)) * ); * console.log(datum); // Some({ tag: "Replete", value: 10 }) * ``` * * @since 2.0.0 */ export function traverse( A: Applicable, ): ( favi: (a: A) => $, ) => (ta: Datum) => $, J, K], [L], [M]> { return (favi) => match( () => A.wrap(constInitial()), () => A.wrap(constPending()), (a) => pipe(favi(a), A.map(replete)), (a) => pipe(favi(a), A.map(refresh)), ); } /** * Create a Showable instance for Datum given a Showable for the inner type. * * @example * ```ts * import { getShowableDatum, replete, initial } from "./datum.ts"; * * const showable = getShowableDatum({ show: (n: number) => n.toString() }); * console.log(showable.show(replete(42))); // "Replete(42)" * console.log(showable.show(initial)); // "Initial" * ``` * * @since 2.0.0 */ export function getShowableDatum({ show }: Showable): Showable> { return ({ show: match( () => `Initial`, () => `Pending`, (a) => `Replete(${show(a)})`, (a) => `Refresh(${show(a)})`, ), }); } /** * Create a Combinable instance for Datum given a Combinable for the inner type. * * @example * ```ts * import { getCombinableDatum, replete } from "./datum.ts"; * import * as N from "./number.ts"; * * const combinable = getCombinableDatum(N.CombinableNumberSum); * const datum1 = replete(2); * const datum2 = replete(3); * const result = combinable.combine(datum2)(datum1); // Replete(5) * ``` * * @since 2.0.0 */ export function getCombinableDatum( S: Combinable, ): Combinable> { return ({ combine: (second) => match( () => second, () => toLoading(second), (v) => isSome(second) ? (isRefresh(second) ? refresh(S.combine(second.value)(v)) : replete(S.combine(second.value)(v))) : (isPending(second) ? refresh(v) : replete(v)), (v) => isSome(second) ? refresh(S.combine(second.value)(v)) : refresh(v), ), }); } /** * Create an Initializable instance for Datum given an Initializable for the inner type. * * @example * ```ts * import { getInitializableDatum } from "./datum.ts"; * import * as N from "./number.ts"; * * const initializable = getInitializableDatum(N.InitializableNumberSum); * const init = initializable.init(); // Initial * ``` * * @since 2.0.0 */ export function getInitializableDatum( S: Initializable, ): Initializable> { return ({ init: constInitial, ...getCombinableDatum(S), }); } /** * Create a Comparable instance for Datum given a Comparable for the inner type. * * @example * ```ts * import { getComparableDatum, replete, initial } from "./datum.ts"; * import * as N from "./number.ts"; * * const comparable = getComparableDatum(N.ComparableNumber); * const datum1 = replete(5); * const datum2 = replete(3); * const result = comparable.compare(datum2)(datum1); // false (5 !== 3) * ``` * * @since 2.0.0 */ export function getComparableDatum(S: Comparable): Comparable> { return ({ compare: (b) => match( () => isInitial(b), () => isPending(b), (v) => isReplete(b) ? S.compare(b.value)(v) : false, (v) => isRefresh(b) ? S.compare(b.value)(v) : false, ), }); } /** * Create a Sortable instance for Datum given a Sortable for the inner type. * * @example * ```ts * import { getSortableDatum, replete, initial } from "./datum.ts"; * import * as N from "./number.ts"; * import * as A from "./array.ts"; * import { pipe } from "./fn.ts"; * * const sortable = getSortableDatum(N.SortableNumber); * const data = [replete(3), initial, replete(1)]; * const sorted = pipe( * data, * A.sort(sortable) * ); * // [initial, replete(1), replete(3)] * ``` * * @since 2.0.0 */ export function getSortableDatum(O: Sortable): Sortable> { return fromSort((fst, snd) => pipe( fst, match( () => isInitial(snd) ? 0 : -1, () => isInitial(snd) ? 1 : isPending(snd) ? 0 : -1, (value) => isNone(snd) ? 1 : isReplete(snd) ? O.sort(value, snd.value) : -1, (value) => isRefresh(snd) ? O.sort(value, snd.value) : 1, ), ) ); } /** * @since 2.0.0 */ export const ApplicableDatum: Applicable = { apply, map, wrap, }; /** * @since 2.0.0 */ export const MappableDatum: Mappable = { map, }; /** * @since 2.0.0 */ export const FlatmappableDatum: Flatmappable = { apply, flatmap, map, wrap, }; /** * @since 2.0.0 */ export const TraversableDatum: Traversable = { map, fold, traverse, }; /** * @since 2.0.0 */ export const WrappableDatum: Wrappable = { wrap, }; /** * @since 2.0.0 */ export const FilterableDatum: Filterable = { filter, filterMap, partition, partitionMap, }; /** * @since 2.0.0 */ export const tap: Tap = createTap(FlatmappableDatum); /** * @since 2.0.0 */ export const bind: Bind = createBind(FlatmappableDatum); /** * @since 2.0.0 */ export const bindTo: BindTo = createBindTo(MappableDatum);