/** * Data is an ADT which allows you to represent all the states involved in loading a * piece of data asynchronously. * * @since 0.9.2 */ import * as AD from 'fp-ts/Alt' import * as Alternative_ from 'fp-ts/Alternative' import * as App from 'fp-ts/Applicative' import * as Ap from 'fp-ts/Apply' import * as Ch from 'fp-ts/Chain' import { Compactable1 } from 'fp-ts/Compactable' import * as Ei from 'fp-ts/Either' import { Eq } from 'fp-ts/Eq' import { Filterable1 } from 'fp-ts/Filterable' import { Foldable1 } from 'fp-ts/Foldable' import { constant, constFalse, flow, identity, Lazy, pipe } from 'fp-ts/function' import * as F from 'fp-ts/Functor' import { HKT } from 'fp-ts/HKT' import { Monad1 } from 'fp-ts/Monad' import { Monoid } from 'fp-ts/Monoid' import * as O from 'fp-ts/Option' import { Pointed1 } from 'fp-ts/Pointed' import { Predicate } from 'fp-ts/Predicate' import { Semigroup } from 'fp-ts/Semigroup' import { Separated } from 'fp-ts/Separated' import { Show } from 'fp-ts/Show' import { Traversable1 } from 'fp-ts/Traversable' import * as P from './Progress' /** * @since 0.9.2 * @category Model */ export type Data = NoData | Loading | Refresh | Replete /** * @since 0.9.2 * @category Type-level */ export type Value = [A] extends [Data] ? R : never /** * @since 0.9.2 * @category Refinement */ export const isNoData = (data: Data): data is NoData => data._tag === 'NoData' /** * @since 0.9.2 * @category Refinement */ export const isLoading = (data: Data): data is Loading => data._tag === 'Loading' /** * @since 0.9.2 * @category Refinement */ export const isRefresh = (data: Data): data is Refresh => data._tag === 'Refresh' /** * @since 0.9.2 * @category Refinement */ export const isReplete = (data: Data): data is Replete => data._tag === 'Replete' /** * @since 0.9.2 * @category Refinement */ export const hasValue = (data: Data): data is Refresh | Replete => isRefresh(data) || isReplete(data) /** * @since 0.9.2 * @category Model */ export interface NoData { readonly _tag: 'NoData' } /** * @since 0.9.2 * @category Constructor */ export const noData: NoData = { _tag: 'NoData' } /** * @since 0.9.2 * @category Model */ export interface Loading { readonly _tag: 'Loading' readonly progress: O.Option } /** * @since 0.9.2 * @category Constructor */ export const loading: Loading = { _tag: 'Loading', progress: O.none, } /** * @since 0.9.2 * @category Constructor */ export const fromProgress = (progress: P.Progress): Loading => ({ _tag: 'Loading', progress: O.some(progress), }) /** * @since 0.9.2 * @category Model */ export interface Refresh { readonly _tag: 'Refresh' readonly value: A readonly progress: O.Option } /** * @since 0.9.2 * @category Constructor */ export const refresh = (value: A, progress: O.Option = O.none): Refresh => ({ _tag: 'Refresh', value, progress, }) /** * @since 0.9.2 * @category Model */ export interface Replete { readonly _tag: 'Replete' readonly value: A } /** * @since 0.9.2 * @category Constructor */ export const replete = (value: A): Replete => ({ _tag: 'Replete', value }) /** * @since 0.9.2 * @category Deconstructor */ export const matchW = ( onNoData: () => A, onLoading: (progress: O.Option) => B, onRefresh: (value: C, progress: O.Option) => D, onReplete: (value: C) => E, ) => (data: Data): A | B | D | E => { switch (data._tag) { case 'NoData': return onNoData() case 'Loading': return onLoading(data.progress) case 'Refresh': return onRefresh(data.value, data.progress) case 'Replete': return onReplete(data.value) } } /** * @since 0.9.2 * @category Deconstructor */ export const match3W = ( onNoData: () => A, onLoading: (progress: O.Option) => B, onRefreshOrReplete: (value: C) => D, ) => (data: Data): A | B | D => { switch (data._tag) { case 'NoData': return onNoData() case 'Loading': return onLoading(data.progress) case 'Refresh': case 'Replete': return onRefreshOrReplete(data.value) } } /** * @since 0.9.2 * @category Deconstructor */ export const match: ( onNoData: () => A, onLoading: () => A, onRefresh: (value: B) => A, onReplete: (value: B) => A, ) => (data: Data) => A = matchW /** * @since 0.9.2 * @category Combinator */ export const toLoading = (data: Data): Data => pipe( data, matchW( () => loading, flow( O.map(fromProgress), O.getOrElse(() => loading), ), refresh, refresh, ), ) /** * @since 0.9.2 * @category Constructor */ export const fromNullable = (a: A | null | undefined): Data => a === null || a === undefined ? noData : replete(a) /** * @since 0.9.2 * @category Typeclass Constructor */ export const getShow = (S: Show): Show> => ({ show: matchW( constant('NoData'), O.matchW( () => `Loading`, (p) => `Loading(${P.Show.show(p)})`, ), (v, p) => pipe( p, O.matchW( () => `Refresh(${S.show(v)})`, (p) => `Refresh(${S.show(v)}, ${P.Show.show(p)})`, ), ), (v) => `Replete(${S.show(v)})`, ), }) /** * @since 0.9.2 * @category Typeclass Constructor */ export const getEq = (S: Eq): Eq> => { const OptionProgressEq = O.getEq(P.Eq) return { equals: (a) => (b) => pipe( a, matchW( () => isNoData(b), (p) => isLoading(b) && OptionProgressEq.equals(p)(b.progress), (a, p) => isRefresh(b) && S.equals(a)(b.value) && OptionProgressEq.equals(p)(b.progress), (a) => isReplete(b) && S.equals(a)(b.value), ), ), } } /** * @since 0.9.2 * @category Typeclass Constructor */ export const getSemigroup = (S: Semigroup): Semigroup> => { const OptionProgressSemigroup = O.getMonoid(P.Semigroup) return { concat: (secondD: Data) => (firstD: Data): Data => pipe( firstD, matchW( constant(secondD), // Empty value (fp) => pipe( secondD, matchW( constant(firstD), constant(firstD), (second, sp) => refresh(second, OptionProgressSemigroup.concat(sp)(fp)), refresh, ), ), (first, fp) => pipe( secondD, matchW( constant(firstD), constant(firstD), (second, sp) => refresh(pipe(first, S.concat(second)), OptionProgressSemigroup.concat(sp)(fp)), (second) => refresh(pipe(first, S.concat(second)), fp), ), ), (first) => pipe( secondD, matchW( constant(firstD), (sp) => refresh(first, sp), (second, sp) => refresh(pipe(first, S.concat(second)), sp), (second) => replete(pipe(first, S.concat(second))), ), ), ), ), } } /** * @since 0.9.2 * @category Typeclass Constructor */ export const getMonoid = (S: Semigroup): Monoid> => ({ ...getSemigroup(S), empty: noData, }) /** * @since 0.9.2 * @category Combinator */ export const getOrElse = (onInitial: () => A, onLoading: (progress: O.Option) => A) => (ma: Data): A => match3W(onInitial, onLoading, identity)(ma) /** * @since 0.9.2 * @category Combinator */ export const getOrElseW = (onInitial: () => A, onLoading: (progress: O.Option) => B) => (ma: Data): A | B | C => match3W(onInitial, onLoading, identity)(ma) /** * @since 0.9.2 * @category Combinator */ export const elem = (E: Eq) => (a: A) => (ma: Data): boolean => match3W(constFalse, constFalse, E.equals(a))(ma) /** * @since 0.9.2 * @category Combinator */ export const exists = (predicate: Predicate) => (ma: Data): boolean => pipe(ma, match3W(constFalse, constFalse, predicate)) /** * @since 0.9.2 * @category Constructor */ export const of = (value: A): Data => replete(value) /** * @since 0.9.2 * @category Combinator */ export const map = (f: (value: A) => B) => (data: Data): Data => pipe( data, matchW( constant(noData), flow( O.map(fromProgress), O.getOrElse(() => loading), ), (a, p) => refresh(f(a), p), flow(f, replete), ), ) /** * @since 0.9.2 * @category Combinator */ export const chain = (f: (value: A) => Data) => (data: Data): Data => pipe( data, match3W( constant(noData), flow( O.map(fromProgress), O.getOrElse(() => loading), ), f, ), ) /** * @since 0.9.2 * @category URI */ export const URI = '@typed/fp/Data' /** * @since 0.9.2 * @category URI */ export type URI = typeof URI declare module 'fp-ts/HKT' { export interface URItoKind { [URI]: Data } } /** * @since 0.9.2 * @category Instance */ export const Pointed: Pointed1 = { of, } /** * @since 0.9.2 * @category Instance */ export const Functor: F.Functor1 = { map, } /** * @since 0.9.2 * @category Combinator */ export const bindTo = F.bindTo(Functor) /** * @since 0.9.2 * @category Combinator */ export const flap = F.flap(Functor) /** * @since 0.9.2 * @category Combinator */ export const tupled = F.tupled(Functor) /** * @since 0.9.2 * @category Instance */ export const Chain: Ch.Chain1 = { ...Functor, chain, } /** * @since 0.9.2 * @category Combinator */ export const ap = Ch.ap(Chain) /** * @since 0.9.2 * @category Combinator */ export const bind = Ch.bind(Chain) /** * @since 0.9.2 * @category Combinator */ export const chainFirst = Ch.chainFirst(Chain) /** * @since 0.9.2 * @category Instance */ export const Apply: Ap.Apply1 = { ...Functor, ap, } /** * @since 0.9.2 * @category Combinator */ export const apFirst = Ap.apFirst(Apply) /** * @since 0.9.2 * @category Combinator */ export const apS = Ap.apS(Apply) /** * @since 0.9.2 * @category Combinator */ export const apSecond = Ap.apSecond(Apply) /** * @since 0.9.2 * @category Combinator */ export const apT = Ap.apT(Apply) /** * @since 0.9.2 * @category Typeclass Constructor */ export const getApplySemigroup = Ap.getApplySemigroup(Apply) /** * @since 0.9.2 * @category Instance */ export const Applicative: App.Applicative1 = { ...Apply, ...Pointed, } /** * @since 0.9.2 * @category Typeclass Constructor */ export const getApplicativeMonoid = App.getApplicativeMonoid(Applicative) /** * @since 0.9.2 * @category Instance */ export const Monad: Monad1 = { ...Chain, ...Pointed, } /** * @since 0.9.2 * @category Combinator */ export const chainRec = (f: (value: A) => Data>) => (value: A): Data => { let data = f(value) while (!isNoData(data) && !isLoading(data)) { if (Ei.isRight(data.value)) { return replete(data.value.right) } data = f(data.value.left) } return data } /** * @since 0.9.2 * @category Combinator */ export const alt = (f: Lazy>) => (b: Data): Data => pipe(b, matchW(f, f, refresh, replete)) /** * @since 0.9.2 * @category Instance */ export const Alt: AD.Alt1 = { ...Functor, alt, } /** * @since 0.9.2 * @category Combinator */ export const altAll = AD.altAll(Alt) /** * @since 0.9.2 * @category Constructor */ export const zero = (): Data => noData /** * @since 0.9.2 * @category Instance */ export const Alternative: Alternative_.Alternative1 = { ...Alt, zero, } /** * @since 0.9.2 * @category Natural Transformation */ export const fromOption = (option: O.Option): Data => pipe( option, O.matchW(() => noData, replete), ) /** * @since 0.9.2 * @category Natural Transformation */ export const toOption = (data: Data): O.Option => pipe( data, matchW( () => O.none, () => O.none, O.some, O.some, ), ) /** * @since 0.9.2 * @category Combinator */ export function foldMap(M: Monoid): (f: (a: A) => M) => (fa: Data) => M { return (f) => (fa) => pipe( fa, match3W( () => M.empty, () => M.empty, f, ), ) } /** * @since 0.9.2 * @category Deconstructor */ export const reduce = (seed: A, f: (acc: A, value: B) => A) => (data: Data): A => pipe( data, match3W(constant(seed), constant(seed), (b) => f(seed, b)), ) /** * @since 0.9.2 * @category Deconstructor */ export const reduceRight = (seed: A, f: (value: B, acc: A) => A) => (data: Data): A => pipe( data, match3W(constant(seed), constant(seed), (b) => f(b, seed)), ) /** * @since 0.9.2 * @category Instance */ export const Foldable: Foldable1 = { reduce, foldMap, reduceRight, } /** * @since 0.9.2 * @category Combinator */ export function traverse(F: App.Applicative) { return (f: (value: A) => HKT) => (data: Data): HKT> => pipe( data, matchW( () => F.of(noData), O.matchW( () => F.of(loading), (p) => F.of(fromProgress(p)), ), (a, progress) => pipe( a, f, F.map((b) => refresh(b, progress)), ), flow(f, F.map(replete)), ), ) } /** * @since 0.9.2 * @category Instance */ export const Traversable: Traversable1 = { ...Functor, traverse, } /** * @since 0.9.2 * @category Combinator */ export const compact = (dataOption: Data>): Data => pipe(dataOption, chain(O.matchW(() => noData, replete))) /** * @since 0.9.2 * @category Combinator */ export const separate = (dataEither: Data>): Separated, Data> => { return { left: pipe(dataEither, chain(Ei.matchW(replete, () => noData))), right: pipe(dataEither, chain(Ei.matchW(() => noData, replete))), } } /** * @since 0.9.2 * @category Instance */ export const Compactable: Compactable1 = { compact, separate, } /** * @since 0.9.2 * @category Combinator */ export const partitionMap: Filterable1['partitionMap'] = (f) => (fa) => pipe(fa, map(f), separate) /** * @since 0.9.2 * @category Combinator */ export const partition = flow(Ei.fromPredicate, partitionMap) as Filterable1['partition'] /** * @since 0.9.2 * @category Combinator */ export const filterMap: Filterable1['filterMap'] = (f) => (fa) => pipe(fa, map(f), compact) /** * @since 0.9.2 * @category Combinator */ export const filter = flow(O.fromPredicate, filterMap) as Filterable1['filter'] /** * @since 0.9.2 * @category Instance */ export const Filterable: Filterable1 = { partitionMap, partition, filterMap, filter, }