/**
* 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);