From fce9464eb1f4fa979e304a4a653628f42da4dfb2 Mon Sep 17 00:00:00 2001 From: Brandon Blaylock Date: Wed, 13 Mar 2024 20:09:06 -0700 Subject: [PATCH] feat: add datum_either and contrib/dux These packages do not have testing. They are both pretty sound but unit testing would be a great contribution. --- README.md | 6 +- contrib/dux.ts | 284 +++++++++++++++++++++++++++ datum_either.ts | 506 ++++++++++++++++++++++++++++++++++++++++++++++++ deno.json | 4 +- optic.ts | 26 ++- 5 files changed, 819 insertions(+), 7 deletions(-) create mode 100644 contrib/dux.ts create mode 100644 datum_either.ts diff --git a/README.md b/README.md index a8c2ac4..0108405 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,9 @@ pipe( ## Documentation Documentation is generated for each github tagged release. The latest -documentation can be found [here](https://jsr.io/@baetheus/fun). Following is a list -of the [algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type) -and +documentation can be found [here](https://jsr.io/@baetheus/fun). Following is a +list of the +[algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type) and [algebraic structures](https://en.wikipedia.org/wiki/Algebraic_structure)/[type classes](https://en.wikipedia.org/wiki/Type_class) that are implemented in fun. Note that some of these types are bote data structures and more general algebraic structures. diff --git a/contrib/dux.ts b/contrib/dux.ts new file mode 100644 index 0000000..14e763a --- /dev/null +++ b/contrib/dux.ts @@ -0,0 +1,284 @@ +import type { DatumEither } from "../datum_either.ts"; +import type { Lens } from "../optic.ts"; + +import * as D from "../datum.ts"; +import * as DE from "../datum_either.ts"; +import * as O from "../optic.ts"; + +import { pipe } from "../fn.ts"; + +// ======= +// Actions +// ====== + +/** + * The bare minimum interface for actions in the dux system. + * If your existing store doesn't have actions with a type parameter + * that you can switch on then dux won't work (at least with typescript). + * + * @since 2.1.0 + */ +export type ActionType = { + readonly type: string; +}; + +/** + * Interface for FSA Action. + * + * @since 2.1.0 + */ +export interface Action

extends ActionType { + readonly value: P; + readonly error: boolean; +} + +/** + * Interface for action matcher property + * + * @since 2.1.0 + */ +export type ActionMatcher

= { + readonly match: (action: ActionType) => action is Action

; +}; + +/** + * Interface for action creator function + * + * @since 2.1.0 + */ +export type ActionFunction

= (payload: P) => Action

; + +/** + * Interface for action creator intersection + * + * @since 2.1.0 + */ +export type ActionCreator

= + & ActionFunction

+ & ActionMatcher

+ & ActionType; + +/** + * Extract an Action type from an ActionCreator + * + * @since 8.0.0 + */ +export type ExtractAction = T extends ActionCreator[] ? Action

+ : never; + +/** + * Interface for "Success" Action payload. + * + * @since 2.1.0 + */ +export interface Success { + readonly params: P; + readonly result: R; +} + +/** + * Interface for "Failure" Action payload. + * + * @since 2.1.0 + */ +export interface Failure { + readonly params: P; + readonly error: E; +} +/** + * Interface for async action creator + * + * @since 2.1.0 + */ +export interface AsyncActionCreators< + P, + R = unknown, + E = unknown, +> { + readonly pending: ActionCreator

; + readonly success: ActionCreator>; + readonly failure: ActionCreator>; +} + +/** + * Interface for the action creator bundle. + * + * @since 2.1.0 + */ +export type ActionCreatorBundle = { + simple:

(type: string) => ActionCreator

; + async: ( + type: string, + ) => AsyncActionCreators; + group: G; +}; + +/** + * @since 2.1.0 + */ +export function collapseType(...types: string[]): string { + return types.length > 0 ? types.join("/") : "UNKNOWN_TYPE"; +} + +function matcherFactory

(type: string): ActionMatcher

{ + return { + match: (action: ActionType): action is Action

=> action.type === type, + }; +} + +function tagFactory(...tags: string[]): ActionType { + return { type: collapseType(...tags) }; +} + +/** + * The simplest way to create an action. + * Generally, for all but the simplest of applications, using + * actionCreatorsFactory is a better move. + * + * @since 7.0.0 + */ +export function actionFactory

(type: string): ActionFunction

{ + return ((value: P) => ({ type, value })) as ActionFunction

; +} + +/** + * General action creator factory + * + * @since 2.1.0 + */ +function actionCreator

( + tag: string, +): ActionCreator

{ + return Object.assign( + actionFactory

(tag), + matcherFactory

(tag), + tagFactory(tag), + ); +} + +/** + * Async action creator factory + * + * @since 2.1.0 + */ +function asyncActionsCreator( + group: string, +): AsyncActionCreators { + return { + pending: actionCreator

(collapseType(group, "PENDING")), + failure: actionCreator>(collapseType(group, "FAILURE")), + success: actionCreator>(collapseType(group, "SUCCESS")), + }; +} + +/** + * General action group creator (wraps other action creators into a group) + * + * @since 2.1.0 + */ +export function actionCreatorFactory( + group: G, +): ActionCreatorBundle { + return { + group, + simple:

(type: string) => actionCreator

(collapseType(group, type)), + async: (type: string) => + asyncActionsCreator(collapseType(group, type)), + }; +} + +// ======== +// Reducers +// ======== + +/** + * Reducer Interface + * + * @since 2.1.0 + */ +export type Reducer = (s: S, a: A) => S; + +/** + * Case function matches ActionCreator to Reducer. + * + * @since 2.1.0 + */ +export function caseFn( + action: ActionCreator

, + reducer: Reducer>, +): Reducer { + return (s, a) => (action.match(a) ? reducer(s, a) : s); +} + +/** + * Case function matches multiple ActionCreators to a Reducer. + * + * @since 2.1.0 + */ +export function casesFn[]>( + actionCreators: A, + reducer: Reducer>, +): Reducer { + return (s, a) => + actionCreators.some(({ match }) => match(a)) + ? reducer(s, > a) + : s; +} + +/** + * Compose caseFn and casesFn. + * + * @since 2.1.0 + */ +export function reducerFn( + ...cases: Array> +): Reducer { + return (state, action) => cases.reduce((s, r) => r(s, action), state); +} + +/** + * Compose caseFn and casesFn with initial state. + * + * @since 2.1.0 + */ +export function reducerDefaultFn( + initialState: S, + ...cases: Array> +): Reducer { + return (state = initialState, action) => + cases.reduce((s, r) => r(s, action), state); +} + +/** + * Generate a reducer that wraps a single DatumEither store value + * + * @since 2.1.0 + */ +export function asyncReducerFactory( + action: AsyncActionCreators, + lens: Lens>, +): Reducer { + return reducerFn( + caseFn(action.pending, pipe(lens, O.modify(D.toLoading))), + caseFn( + action.success, + (s, a) => pipe(lens, O.replace(DE.success(a.value.result)))(s), + ), + caseFn( + action.failure, + (s, a) => pipe(lens, O.replace(DE.failure(a.value.error)))(s), + ), + ); +} + +/** + * Filters actions by first section of action type to bypass sections of the store + * + * @since 7.1.0 + */ +export const filterReducer = ( + match: string, + reducer: Reducer, +): Reducer => +(state, action) => + action.type.startsWith(match) ? reducer(state, action) : state; diff --git a/datum_either.ts b/datum_either.ts new file mode 100644 index 0000000..305c89a --- /dev/null +++ b/datum_either.ts @@ -0,0 +1,506 @@ +/** + * The DatumEither datastructure represents an asynchronous operation that can + * fail. At its heart it is implemented as `() => Promise>`. This + * thunk makes it a performant but lazy operation at the expense of stack + * safety. + * + * @module DatumEither + * @since 2.1.0 + */ + +import type { Kind, Out } from "./kind.ts"; +import type { Applicable } from "./applicable.ts"; +import type { Bimappable } from "./bimappable.ts"; +import type { Bind, Flatmappable } from "./flatmappable.ts"; +import type { BindTo, Mappable } from "./mappable.ts"; +import type { Combinable } from "./combinable.ts"; +import type { Comparable } from "./comparable.ts"; +import type { Datum } from "./datum.ts"; +import type { Either } from "./either.ts"; +import type { Failable, Tap } from "./failable.ts"; +import type { Initializable } from "./initializable.ts"; +import type { Option } from "./option.ts"; +import type { Showable } from "./showable.ts"; +import type { Sortable } from "./sortable.ts"; +import type { Wrappable } from "./wrappable.ts"; + +import * as E from "./either.ts"; +import * as D from "./datum.ts"; +import { none, some } from "./option.ts"; +import { apply as createApply } from "./applicable.ts"; +import { createBind } from "./flatmappable.ts"; +import { createBindTo } from "./mappable.ts"; +import { createTap } from "./failable.ts"; +import { handleThrow, pipe } from "./fn.ts"; +import { isNotNil } from "./nil.ts"; + +/** + * The DatumEither type can best be thought of as an asynchronous function that + * returns an `Either`. ie. `async () => Promise>`. This + * forms the basis of most Promise based asynchronous communication in + * TypeScript. + * + * @since 2.1.0 + */ +export type DatumEither = Datum>; + +/** + * @since 2.0.0 + */ +export type Success = D.Refresh> | D.Replete>; + +/** + * @since 2.0.0 + */ +export type Failure = D.Refresh> | D.Replete>; + +/** + * Specifies DatumEither as a Higher Kinded Type, with covariant + * parameter A corresponding to the 0th index of any substitutions and covariant + * parameter B corresponding to the 1st index of any substitutions. + * + * @since 2.1.0 + */ +export interface KindDatumEither extends Kind { + readonly kind: DatumEither, Out>; +} + +/** + * Constructs a DatumEither from a value and wraps it in an inner *Left* + * traditionally signaling a failure. + * + * @example + * ```ts + * import * as DE from "./datum_either.ts"; + * + * const left = DE.left(1); + * ``` + * + * @since 2.1.0 + */ +export function left(left: B): DatumEither { + return D.wrap(E.left(left)); +} + +/** + * Constructs a DatumEither from a value and wraps it in an inner *Right* + * traditionally signaling a successful computation. + * + * @example + * ```ts + * import * as DE from "./datum_either.ts"; + * + * const right = DE.right(1); + * ``` + * + * @since 2.1.0 + */ +export function right(right: A): DatumEither { + return D.wrap(E.right(right)); +} + +/** + * @since 2.1.0 + */ +export const initial: D.Initial = { tag: "Initial" }; + +/** + * @since 2.1.0 + */ +export const pending: D.Pending = { tag: "Pending" }; + +/** + * @since 2.1.0 + */ +export function success( + value: A, + refresh: boolean = false, +): DatumEither { + return refresh ? D.refresh(E.right(value)) : D.replete(E.right(value)); +} + +/** + * @since 2.1.0 + */ +export function failure( + value: B, + refresh: boolean = false, +): DatumEither { + return refresh ? D.refresh(E.left(value)) : D.replete(E.left(value)); +} + +/** + * @since 2.1.0 + */ +export function constInitial(): DatumEither { + return initial; +} + +/** + * @since 2.1.0 + */ +export function constPending(): DatumEither { + return pending; +} + +/** + * @since 2.1.0 + */ +export function fromNullable(a: A): DatumEither> { + return isNotNil(a) ? success(a as NonNullable) : initial; +} +/** + * @since 2.1.0 + */ +export function match( + onInitial: () => O, + onPending: () => O, + onFailure: (b: B, refresh: boolean) => O, + onSuccess: (a: A, refresh: boolean) => O, +): (ua: DatumEither) => O { + return (ua) => { + switch (ua.tag) { + case "Initial": + return onInitial(); + case "Pending": + return onPending(); + case "Refresh": + return pipe( + ua.value, + E.match((b) => onFailure(b, true), (a) => onSuccess(a, true)), + ); + case "Replete": + return pipe( + ua.value, + E.match((b) => onFailure(b, false), (a) => onSuccess(a, false)), + ); + } + }; +} + +/** + * Wraps a Datum of A in a try-catch block which upon failure returns B instead. + * Upon success returns a *Right* and *Left* for a failure. + * + * @since 2.1.0 + */ +export function tryCatch( + fasr: (...as: AS) => A, + onThrow: (e: unknown, as: AS) => B, +): (...as: AS) => DatumEither { + return handleThrow( + fasr, + (a) => right(a), + (e: unknown, as) => left(onThrow(e, as)), + ); +} + +/** + * @since 2.0.0 + */ +export function isSuccess(ua: DatumEither): ua is Success { + return D.isSome(ua) && E.isRight(ua.value); +} + +/** + * @since 2.0.0 + */ +export function isFailure(ua: DatumEither): ua is Failure { + return D.isSome(ua) && E.isLeft(ua.value); +} + +/** + * Lift an always succeeding async computation (Datum) into a DatumEither. + * + * @example + * ```ts + * import * as DE from "./datum_either.ts"; + * import * as D from "./datum.ts"; + * + * const value = DE.fromDatum(D.wrap(1)); + * ``` + * + * @since 2.1.0 + */ +export function fromDatum(ta: Datum): DatumEither { + return pipe(ta, D.map(E.right)); +} + +/** + * Lifts an Either into a DatumEither. + * + * @example + * ```ts + * import * as DE from "./datum_either.ts"; + * import * as E from "./either.ts"; + * + * const value1 = DE.fromEither(E.right(1)); + * const value2 = DE.fromEither(E.left("Error!")); + * ``` + * + * @since 2.1.0 + */ +export function fromEither(ta: Either): DatumEither { + return D.wrap(ta); +} + +/** + * @since 2.1.0 + */ +export function getSuccess(ua: DatumEither): Option { + return isSuccess(ua) ? some(ua.value.right) : none; +} + +/** + * @since 2.1.0 + */ +export function getFailure(ua: DatumEither): Option { + return isFailure(ua) ? some(ua.value.left) : none; +} + +/** + * Construct an DatumEither from a value D. + * + * @example + * ```ts + * import * as DE from "./datum_either.ts"; + * + * const value = DE.wrap(1); + * ``` + * + * @since 2.1.0 + */ +export function wrap(a: A): DatumEither { + return right(a); +} + +/** + * Construct an DatumEither from a value B. + * + * @example + * ```ts + * import * as DE from "./datum_either.ts"; + * + * const value = DE.fail("Error!"); + * ``` + * + * @since 2.1.0 + */ +export function fail(b: B): DatumEither { + return left(b); +} + +/** + * Map a function over the *Right* side of a DatumEither + * + * @since 2.1.0 + */ +export function map( + fai: (a: A) => I, +): (ta: DatumEither) => DatumEither { + return (ta) => pipe(ta, D.map(E.map(fai))); +} + +/** + * Map a function over the *Left* side of a DatumEither + * + * @since 2.1.0 + */ +export function mapSecond( + fbj: (b: B) => J, +): (ta: DatumEither) => DatumEither { + return (ta) => pipe(ta, D.map(E.mapSecond(fbj))); +} + +/** + * TODO: revisit createApply to align types. + * + * @since 2.1.0 + */ +const _apply: Applicable["apply"] = createApply( + D.ApplicableDatum, + E.ApplicableEither, +) as Applicable["apply"]; + +/** + * Apply an argument to a function under the *Right* side. + * + * @since 2.1.0 + */ +export function apply( + ua: DatumEither, +): (ufai: DatumEither I>) => DatumEither { + return _apply(ua); +} + +/** + * Chain DatumEither based computations together in a pipeline + * + * @since 2.1.0 + */ +export function flatmap( + fati: (a: A) => DatumEither, +): (ua: DatumEither) => DatumEither { + return match( + // Cast to any as TS does not infer the B in B | J + // deno-lint-ignore no-explicit-any + constInitial, + constPending, + failure, + (a, refresh) => refresh ? D.toLoading(fati(a)) : fati(a), + ); +} + +/** + * Chain DatumEither based failures, *Left* sides, useful for recovering + * from error conditions. + * + * @since 2.1.0 + */ +export function recover( + fbtj: (b: B) => DatumEither, +): (ta: DatumEither) => DatumEither { + return match( + // Cast to any as TS does not infer the A in A | I + // deno-lint-ignore no-explicit-any + constInitial, + constPending, + (b, refresh) => refresh ? D.toLoading(fbtj(b)) : fbtj(b), + success, + ); +} + +/** + * Provide an alternative for a failed computation. + * Useful for implementing defaults. + * + * @since 2.1.0 + */ +export function alt( + ui: DatumEither, +): (ta: DatumEither) => DatumEither { + return (ua) => isFailure(ua) ? ui : ua; +} + +/** + * @since 2.1.0 + */ +export function getShowableDatumEither( + CA: Showable, + CB: Showable, +): Showable> { + return D.getShowableDatum(E.getShowableEither(CB, CA)); +} + +/** + * @since 2.1.0 + */ +export function getCombinableDatumEither( + CA: Combinable, + CB: Combinable, +): Combinable> { + return D.getCombinableDatum(E.getCombinableEither(CA, CB)); +} + +/** + * @since 2.1.0 + */ +export function getInitializableDatumEither( + CA: Initializable, + CB: Initializable, +): Initializable> { + return D.getInitializableDatum(E.getInitializableEither(CA, CB)); +} + +/** + * @since 2.1.0 + */ +export function getComparableDatumEither( + CA: Comparable, + CB: Comparable, +): Comparable> { + return D.getComparableDatum(E.getComparableEither(CB, CA)); +} + +/** + * @since 2.1.0 + */ +export function getSortableDatumEither( + CA: Sortable, + CB: Sortable, +): Sortable> { + return D.getSortableDatum(E.getSortableEither(CB, CA)); +} + +/** + * @since 2.1.0 + */ +export const ApplicableDatumEither: Applicable = { + apply, + map, + wrap, +}; + +/** + * @since 2.1.0 + */ +export const BimappableDatumEither: Bimappable = { + map, + mapSecond, +}; + +/** + * @since 2.1.0 + */ +export const FlatmappableDatumEither: Flatmappable = { + wrap, + apply, + map, + flatmap, +}; + +/** + * @since 2.1.0 + */ +export const FailableDatumEither: Failable = { + wrap, + apply, + map, + flatmap, + alt, + fail, + recover, +}; + +/** + * @since 2.1.0 + */ +export const MappableDatumEither: Mappable = { + map, +}; + +/** + * @since 2.1.0 + */ +export const WrappableDatumEither: Wrappable = { + wrap, +}; + +/** + * @since 2.1.0 + */ +export const tap: Tap = createTap(FailableDatumEither); + +/** + * @since 2.1.0 + */ +export const bind: Bind = createBind( + FlatmappableDatumEither, +); + +/** + * @since 2.1.0 + */ +export const bindTo: BindTo = createBindTo( + FlatmappableDatumEither, +); diff --git a/deno.json b/deno.json index db6dcd4..a38ead1 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@baetheus/fun", - "version": "2.0.2", + "version": "2.1.0", "exports": { "./applicable": "./applicable.ts", "./array": "./array.ts", @@ -13,6 +13,7 @@ "./comparable": "./comparable.ts", "./composable": "./composable.ts", "./datum": "./datum.ts", + "./datum_either": "./datum_either.ts", "./decoder": "./decoder.ts", "./either": "./either.ts", "./failable": "./failable.ts", @@ -51,6 +52,7 @@ "./traversable": "./traversable.ts", "./tree": "./tree.ts", "./wrappable": "./wrappable.ts", + "./contrib/dux": "./contrib/dux.ts", "./contrib/fast-check": "./contrib/fast-check.ts", "./contrib/free": "./contrib/free.ts", "./contrib/most": "./contrib/most.ts" diff --git a/optic.ts b/optic.ts index 3b54001..8e13646 100644 --- a/optic.ts +++ b/optic.ts @@ -30,6 +30,7 @@ import type { $, Kind } from "./kind.ts"; import type { Comparable } from "./comparable.ts"; +import type { DatumEither } from "./datum_either.ts"; import type { Initializable } from "./initializable.ts"; import type { Either } from "./either.ts"; import type { Flatmappable } from "./flatmappable.ts"; @@ -41,13 +42,14 @@ import type { Refinement } from "./refinement.ts"; import type { Traversable } from "./traversable.ts"; import type { Tree } from "./tree.ts"; -import * as I from "./identity.ts"; -import * as O from "./option.ts"; import * as A from "./array.ts"; -import * as R from "./record.ts"; +import * as DE from "./datum_either.ts"; import * as E from "./either.ts"; +import * as I from "./identity.ts"; import * as M from "./map.ts"; +import * as O from "./option.ts"; import * as P from "./pair.ts"; +import * as R from "./record.ts"; import { TraversableSet } from "./set.ts"; import { TraversableTree } from "./tree.ts"; import { isNotNil } from "./nil.ts"; @@ -1520,3 +1522,21 @@ export const first: ( export const second: ( optic: Optic>, ) => Optic, S, B> = compose(lens(P.getSecond, P.mapSecond)); + +/** + * @since 2.1.0 + */ +export const success: ( + optic: Optic>, +) => Optic, S, A> = compose( + prism(DE.getSuccess, DE.success, DE.map), +); + +/** + * @since 2.1.0 + */ +export const failure: ( + optic: Optic>, +) => Optic, S, B> = compose( + prism(DE.getFailure, DE.failure, DE.mapSecond), +);