diff --git a/README.md b/README.md index 0108405..1036e1e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ 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 +that are implemented in fun. Note that some of these types are both data structures and more general algebraic structures. | Type | Algebraic Data Type | Algebraic Structure | Native | Other Names | @@ -102,6 +102,7 @@ structures and more general algebraic structures. | [Predicate](./predicate.ts) | ✓ | | | | | [Refinement](./refinement.ts) | ✓ | | | | | [State](./state.ts) | ✓ | | | | +| [Stream](./stream.ts) | ✓ | | | Observable | | [Sync](./sync.ts) | ✓ | | | IO | | [SyncEither](./sync_either.ts) | ✓ | | | IOEither | | [These](./these.ts) | ✓ | | | | @@ -110,10 +111,10 @@ structures and more general algebraic structures. ## Major Versions In the fashion of semantic versioning function makes an effort to not break APIs -on minor or patch releases. Occasionally, candidate tags (2.0.0-alpha.1) will be -used to indicate a commit is ready for inspection by other developers of fun. -The main branch of fun is for bleeding edge developement and is not considered -to be a production ready import. +on minor or patch releases. Occasionally, candidate tags (eg. 2.0.0-alpha.1) +will be used to indicate a commit is ready for inspection by other developers of +fun. The main branch of fun is for bleeding edge developement and is not +considered to be a production ready import. | Version | Deno Release | TypeScript Version | | ------- | --------------------------------------------------------------- | -------------------------------------------------------------------- | @@ -125,7 +126,7 @@ to be a production ready import. functional started as an exploratory project in late 2020 to learn more about higher kinded type implementations in TypeScript and to assess how much effort it would take to port fp-ts to a Deno-native format. Through that process it -became clear that the things I had learned could serve as both a useful tool and +became clear that the things I had learned could serve as both useful tools and as a learning resource in and of itself. At various times functional has used multiple hkt encodings, type class definitions, and implementation methods. Some of the key history moments of functional are in the hkts history. Specifically, @@ -169,9 +170,4 @@ start. Last, if you wish to add a feature it's good to start a discussion about the feature with a few concrete use cases before submitting a PR. This will allow for others to chime in without crowding the issues section. -Also, primary development takes places on one of my servers where I use fossil -instead of git as a VCS. I still use github for interfacing with users and for -releases, but if you wish to become a long term contributor learning to get -around with fossil is a must. - Thanks for you interest! diff --git a/applicable.ts b/applicable.ts index c20d970..e9aff45 100644 --- a/applicable.ts +++ b/applicable.ts @@ -20,13 +20,25 @@ import type { Wrappable } from "./wrappable.ts"; * * @since 2.0.0 */ -export interface Applicable - extends Mappable, Wrappable, Hold { - readonly apply: ( +export interface Applicable< + U extends Kind, +> extends Mappable, Wrappable, Hold { + readonly apply: < + A, + B = never, + C = never, + D = unknown, + E = unknown, + >( ta: $, - ) => ( - tfai: $ I, J, K], [D], [E]>, - ) => $; + ) => < + I, + J = never, + K = never, + L = unknown, + >( + tfai: $ I, J, K], [L], [E]>, + ) => $; } /** diff --git a/array.ts b/array.ts index aa09e4e..0ca0470 100644 --- a/array.ts +++ b/array.ts @@ -70,7 +70,7 @@ export type AnyNonEmptyArray = NonEmptyArray; * * @since 2.0.0 */ -export interface KindArray extends Kind { +export interface KindReadonlyArray extends Kind { readonly kind: ReadonlyArray>; } @@ -849,7 +849,7 @@ type Sequence[]> = $ = []>( +export type SequenceArray = []>( ...uas: US ) => Sequence; @@ -874,7 +874,7 @@ export type SequenceArray = []>( */ export function sequence( A: Applicable, -): []>( +): []>( ...ua: VS ) => Sequence { // deno-lint-ignore no-explicit-any @@ -1203,6 +1203,33 @@ export function binarySearch( }; } +export function monoSearch( + { sort }: Sortable, +): (value: A, sorted: ReadonlyArray) => number { + return (value, sorted) => { + if (sorted.length === 0) { + return 0; + } + + let bot = 0; + let mid: number; + let top = sorted.length; + let ordering; + + while (top > 1) { + mid = Math.floor(top / 2); + ordering = sort(value, sorted[bot + mid]); + + if (ordering >= 0) { + bot += mid; + } + top -= mid; + } + + return sort(value, sorted[bot]) === 0 ? bot : bot + 1; + }; +} + /** * Given an Sortable construct a curried insert function that inserts values into * a new array in a sorted fashion. Internally this uses binarySearch to find @@ -1474,7 +1501,7 @@ export function getInitializableArray(): Initializable< /** * @since 2.0.0 */ -export const ApplicableArray: Applicable = { +export const ApplicableArray: Applicable = { apply, map, wrap, @@ -1483,7 +1510,7 @@ export const ApplicableArray: Applicable = { /** * @since 2.0.0 */ -export const FilterableArray: Filterable = { +export const FilterableArray: Filterable = { filter, filterMap, partition, @@ -1493,7 +1520,7 @@ export const FilterableArray: Filterable = { /** * @since 2.0.0 */ -export const FlatmappableArray: Flatmappable = { +export const FlatmappableArray: Flatmappable = { wrap, map, apply, @@ -1503,17 +1530,17 @@ export const FlatmappableArray: Flatmappable = { /** * @since 2.0.0 */ -export const MappableArray: Mappable = { map }; +export const MappableArray: Mappable = { map }; /** * @since 2.0.0 */ -export const FoldableArray: Foldable = { fold }; +export const FoldableArray: Foldable = { fold }; /** * @since 2.0.0 */ -export const TraversableArray: Traversable = { +export const TraversableArray: Traversable = { map, fold, traverse, @@ -1522,19 +1549,19 @@ export const TraversableArray: Traversable = { /** * @since 2.0.0 */ -export const WrappableArray: Wrappable = { wrap }; +export const WrappableArray: Wrappable = { wrap }; /** * @since 2.0.0 */ -export const tap: Tap = createTap(FlatmappableArray); +export const tap: Tap = createTap(FlatmappableArray); /** * @since 2.0.0 */ -export const bind: Bind = createBind(FlatmappableArray); +export const bind: Bind = createBind(FlatmappableArray); /** * @since 2.0.0 */ -export const bindTo: BindTo = createBindTo(MappableArray); +export const bindTo: BindTo = createBindTo(MappableArray); diff --git a/async_iterable.ts b/async_iterable.ts index 0edf7ae..fdc24ae 100644 --- a/async_iterable.ts +++ b/async_iterable.ts @@ -51,6 +51,15 @@ export function fromIterable(ta: Iterable): AsyncIterable { }); } +/** + * @since 2.2.0 + */ +export function fromPromise(ua: Promise): AsyncIterable { + return asyncIterable(async function* () { + yield await ua; + }); +} + /** * @since 2.0.0 */ @@ -70,6 +79,21 @@ export function range( }); } +export function loop( + stepper: (state: S, value: A) => [S, B], + seed: S, +): (ua: AsyncIterable) => AsyncIterable { + return (ua) => + asyncIterable(async function* () { + let hold: S = seed; + for await (const a of ua) { + const [next, value] = stepper(hold, a); + hold = next; + yield value; + } + }); +} + /** * @since 2.0.0 */ @@ -340,18 +364,13 @@ export function takeWhile( * @since 2.0.0 */ export function scan( - foldr: (accumulator: O, value: A, index: number) => O, - initial: O, + scanner: (accumulator: O, value: A) => O, + seed: O, ): (ta: AsyncIterable) => AsyncIterable { - return (ta) => - asyncIterable(async function* () { - let result = initial; - let index = 0; - for await (const a of ta) { - result = foldr(result, a, index++); - yield result; - } - }); + return loop((accumulator, value) => { + const result = scanner(accumulator, value); + return [result, result]; + }, seed); } /** diff --git a/benchmarks/array.bench.ts b/benchmarks/array.bench.ts new file mode 100644 index 0000000..859819a --- /dev/null +++ b/benchmarks/array.bench.ts @@ -0,0 +1,17 @@ +import * as A from "../array.ts"; +import * as N from "../number.ts"; +import { pipe } from "../fn.ts"; + +const count = 100_000; +const sorted = A.range(count); +const binarySearch = A.binarySearch(N.SortableNumber); +const monoSearch = A.monoSearch(N.SortableNumber); +const searches = pipe(A.range(count), A.map(() => Math.random() * count)); + +Deno.bench("array binarySearch", { group: "binarySearch" }, () => { + searches.forEach((value) => binarySearch(value, sorted)); +}); + +Deno.bench("array monoSearch", { group: "binarySearch" }, () => { + searches.forEach((value) => monoSearch(value, sorted)); +}); diff --git a/benchmarks/stream.bench.ts b/benchmarks/stream.bench.ts new file mode 100644 index 0000000..7637c3b --- /dev/null +++ b/benchmarks/stream.bench.ts @@ -0,0 +1,102 @@ +import * as S from "../stream.ts"; +import * as CB from "../ideas/callbag.ts"; +import * as M from "npm:@most/core@1.6.1"; +import * as MS from "npm:@most/scheduler@1.3.0"; +import * as R from "npm:rxjs@7.8.1"; +import { pipe } from "../fn.ts"; + +const count = 1_000_000; +const add = (a: number, b: number) => a + b; +const passthrough = (_: number, value: number) => value; +const runRx = (obs: R.Observable): Promise => + new Promise((resolve, reject) => { + obs.subscribe({ complete: resolve, error: reject }); + }); +function mostRange(count: number) { + return M.newStream((snk) => { + let ended = false; + let index = -1; + while (++index < count) { + if (ended) { + break; + } + snk.event(0, index); + } + if (!ended) { + snk.end(0); + } + return { + dispose: () => { + ended = true; + }, + }; + }); +} + +Deno.bench("stream scan", { group: "scan" }, async () => { + await pipe( + S.range(count), + S.scan(add, 0), + S.scan(passthrough, 0), + S.runPromise(S.DefaultEnv), + ); +}); + +Deno.bench("callbag scan", { group: "scan" }, async () => + await pipe( + CB.range(count), + CB.scan(add, 0), + CB.scan(passthrough, 0), + CB.runPromise({ queueMicrotask }), + )); + +Deno.bench("most scan", { group: "scan" }, async () => + await pipe( + mostRange(count), + M.scan(add, 0), + M.scan(passthrough, 0), + (stream) => + M.runEffects( + stream, + MS.newDefaultScheduler(), + ), + )); + +Deno.bench("rxjs scan", { group: "scan" }, async () => { + await pipe( + R.range(0, count), + R.scan(add, 0), + R.scan(passthrough, 0), + runRx, + ); +}); + +const JOIN_COUNT = 1_000; + +Deno.bench("stream join", { group: "join" }, async () => { + await pipe( + S.range(JOIN_COUNT), + S.flatmap(() => S.range(JOIN_COUNT)), + S.runPromise(S.DefaultEnv), + ); +}); + +Deno.bench("most join", { group: "join" }, async () => { + await pipe( + mostRange(JOIN_COUNT), + M.chain(() => mostRange(JOIN_COUNT)), + (stream) => + M.runEffects( + stream, + MS.newDefaultScheduler(), + ), + ); +}); + +Deno.bench("rxjs join", { group: "join" }, async () => { + await pipe( + R.range(0, JOIN_COUNT), + R.mergeMap(() => R.range(0, JOIN_COUNT)), + runRx, + ); +}); diff --git a/contrib/dux.ts b/contrib/dux.ts deleted file mode 100644 index 77cca22..0000000 --- a/contrib/dux.ts +++ /dev/null @@ -1,417 +0,0 @@ -import type { DatumEither } from "../datum_either.ts"; -import type { Stream } from "./most.ts"; -import type { Lens } from "../optic.ts"; - -import * as A from "../array.ts"; -import * as D from "../datum.ts"; -import * as DE from "../datum_either.ts"; -import * as M from "./most.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; -} - -/** - * 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 2.1.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. - */ -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.value) : s); -} - -/** - * Case function matches multiple ActionCreators to a Reducer. - * - * @since 2.1.0 - */ -export function casesFn[]>( - actionCreators: A, - reducer: Reducer["value"]>, -): Reducer { - return (s, a) => - actionCreators.some(({ match }) => match(a)) - ? reducer(s, (> a).value) - : 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, { result }) => pipe(lens, O.replace(DE.success(result)))(s), - ), - caseFn( - action.failure, - (s, { error }) => pipe(lens, O.replace(DE.failure(error)))(s), - ), - ); -} - -/** - * Filters actions by first section of action type to bypass sections of the store - * - * @since 2.1.0 - */ -export const filterReducer = ( - match: string, - reducer: Reducer, -): Reducer => -(state, action) => - action.type.startsWith(match) ? reducer(state, action) : state; - -// ============ -// MetaReducers -// ============ - -export type MetaReducer = ( - reducer: Reducer, -) => Reducer; - -export function metaReducerFn( - metareducer: MetaReducer, -): MetaReducer { - return metareducer; -} - -// ======= -// Effects -// ======= - -/** - * @since 2.1.2 - */ -export type Effect = ( - a: A, -) => Stream; - -/** - * @since 2.1.2 - */ -export type EffectWide = ( - a: A, -) => ActionType | Promise | Stream; - -function liftAction( - action: ActionType | Promise | Stream, -): Stream { - if ("type" in action) { - return M.wrap(action); - } else if ("then" in action) { - return M.fromPromise(action); - } else { - return action; - } -} - -/** - * @since 2.1.2 - */ -export function caseEff

( - action: ActionCreator

, - effect: EffectWide

, -): Effect { - return (a) => action.match(a) ? liftAction(effect(a.value)) : M.empty(); -} - -/** - * @since 2.1.2 - */ -export function caseEffs[]>( - actionCreators: A, - effect: EffectWide["value"]>, -): Effect { - return (a) => - actionCreators.some(({ match }) => match(a)) - ? liftAction(effect((> a).value)) - : M.empty(); -} - -/** - * @since 2.1.2 - */ -export function effectFn( - ...cases: ReadonlyArray> -): Effect { - return (action) => - pipe( - cases, - A.map((a) => liftAction(a(action))), - M.mergeArray, - ); -} - -// ========== -// MetaEffect -// ========== - -/** - * @since 2.1.2 - */ -export type MetaEffect = ( - effect: Effect, -) => Effect; - -/** - * @since 2.1.2 - */ -export function metaEffectFn( - metaEffect: MetaEffect, -): MetaEffect { - return metaEffect; -} - -// ===== -// Store -// ===== - -/** - * @since 2.1.2 - */ -export type Store = { - readonly state: Stream; - readonly dispatch: (action: ActionType) => void; -}; - -/** - * @since 2.1.2 - */ -export function createStore( - initial: S, - reducer: Reducer, - effect: Effect = () => M.empty(), -): Store { - const [dispatch, dispatched] = M.createAdapter(); - const state = pipe( - dispatched, - M.mergeMapConcurrently( - (a) => M.startWith(a, effect(a)), - Number.POSITIVE_INFINITY, - ), - M.scan(reducer, initial), - ); - - return { state, dispatch }; -} diff --git a/contrib/fast-check.ts b/contrib/fast-check.ts deleted file mode 100644 index 50c00d5..0000000 --- a/contrib/fast-check.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * This module contains a Kind and Schemable for the Arbitrary type from - * fast-check. An Arbitrary can be used to do property based testing as well as - * to generate random data. - * - * @experimental - * @module FastCheck - * @since 2.0.1 - */ -import type * as FC from "npm:fast-check@3.14.0"; -import type { Kind, Out, Spread } from "../kind.ts"; -import type { - LiteralSchemable, - Schemable, - TupleSchemable, -} from "../schemable.ts"; - -/** - * Specifies Arbitrary from fast-check as a Higher Kinded Type. - * - * @since 2.0.0 - */ -export interface KindArbitrary extends Kind { - readonly kind: FC.Arbitrary>; -} - -/** - * Given an API that matches the default exports of fast-check at version 3.14.0 - * create a Schemable for Arbitrary. - * - * @since 2.0.1 - */ -export function getSchemableArbitrary(fc: typeof FC): Schemable { - return { - unknown: fc.anything, - string: fc.string, - number: fc.float, - boolean: fc.boolean, - literal: fc.constantFrom as LiteralSchemable["literal"], - nullable: fc.option, - undefinable: fc.option, - record: (arb: FC.Arbitrary) => fc.dictionary(fc.string(), arb), - array: fc.array, - tuple: fc.tuple as TupleSchemable["tuple"], - struct: fc.record, - partial: (items) => fc.record(items, { requiredKeys: [] }), - intersect: (second) => (first) => - fc.tuple(first, second).map(([first, second]) => - Object.assign({}, first, second) as Spread< - (typeof first) & (typeof second) - > - ), - union: (second) => (first) => fc.oneof(first, second), - - lazy: (_id, builder) => fc.memo(builder)(), - }; -} diff --git a/contrib/most.ts b/contrib/most.ts deleted file mode 100644 index ac76972..0000000 --- a/contrib/most.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { $, Kind, Nest, Out } from "../kind.ts"; -import type { Applicable } from "../applicable.ts"; -import type { Bind, Flatmappable, Tap } from "../flatmappable.ts"; -import type { BindTo, Mappable } from "../mappable.ts"; -import type { Scheduler, Sink, Stream } from "npm:@most/types@1.1.0"; -import type { Wrappable } from "../wrappable.ts"; - -import * as M from "npm:@most/core@1.6.1"; -import { createBind, createTap } from "../flatmappable.ts"; -import { createBindTo } from "../mappable.ts"; -import { flow, pipe } from "../fn.ts"; - -export * from "npm:@most/core@1.6.1"; -export * from "npm:@most/hold@4.1.0"; -export * from "npm:@most/adapter@1.0.0"; -export { newDefaultScheduler, newScheduler } from "npm:@most/scheduler@1.3.0"; - -export interface KindStream extends Kind { - readonly kind: Stream>; -} - -export function count(sa: Stream): Stream { - return keepIndex(withCount(sa)); -} - -export function withCount(sa: Stream): Stream<[number, A]> { - return withIndexStart(1, sa); -} - -export function index(sa: Stream): Stream { - return keepIndex(withIndex(sa)); -} - -export function withIndex(sa: Stream): Stream<[number, A]> { - return withIndexStart(0, sa); -} - -export function withIndexStart( - start: number, - sa: Stream, -): Stream<[number, A]> { - return indexed((i) => [i, i + 1], start, sa); -} - -export function indexed( - f: (s: S) => [I, S], - init: S, - sa: Stream, -): Stream<[I, A]> { - return M.loop( - (s, a) => { - const [index, seed] = f(s); - return { seed, value: [index, a] }; - }, - init, - sa, - ); -} - -export function keepIndex(s: Stream<[I, unknown]>): Stream { - return M.map((ia) => ia[0], s); -} - -export async function collect( - stream: Stream, - scheduler: Scheduler, -): Promise { - const as: A[] = []; - await M.runEffects(pipe(stream, M.tap((a) => as.push(a))), scheduler); - return as; -} - -export function sink( - event: (time: number, value: A) => void, - error: (e: unknown) => void, - end: () => void, -): Sink { - return { event, end, error }; -} - -export const debugSink: Sink = sink( - console.log, - console.error, - console.log, -); - -const noop = () => {}; -export const voidSink: Sink = sink(noop, noop, noop); - -export const wrap: Wrappable["wrap"] = M.now; - -export const map: Mappable["map"] = M.map; - -export const apply: Applicable["apply"] = M.ap; - -export const flatmap: Flatmappable["flatmap"] = (faui) => (ua) => - pipe( - ua, - M.map(faui), - M.switchLatest, - ); - -export const WrappableStream: Wrappable = { wrap }; - -export const MappableStream: Mappable = { map }; - -export const ApplicableStream: Applicable = { wrap, map, apply }; - -export const FlatmappableStream: Flatmappable = { - wrap, - map, - apply, - flatmap, -}; - -export const bind: Bind = createBind(FlatmappableStream); - -export const bindTo: BindTo = createBindTo(FlatmappableStream); - -export const tap: Tap = createTap(FlatmappableStream); - -export interface TransformStream extends Kind { - readonly kind: Stream>; -} - -export function transformStream( - FM: Flatmappable, - extract: < - I, - J = unknown, - K = unknown, - L = never, - M = never, - B = unknown, - C = unknown, - D = never, - E = never, - >( - usua: $>, B, C], [D], [E]>, - ) => Stream<$>, -): Flatmappable> { - return { - wrap: (a) => wrap(FM.wrap(a)), - map: (fai) => map(FM.map(fai)), - apply: M.combine(FM.apply) as Flatmappable>["apply"], - flatmap: (faui) => (sua) => - pipe( - sua, - flatmap(flow(FM.map(faui), extract)), - ), - }; -} - -export type * from "npm:@most/types@1.1.0"; /** Export types */ diff --git a/decoder.ts b/decoder.ts index 9a3a3e4..9f4dece 100644 --- a/decoder.ts +++ b/decoder.ts @@ -946,7 +946,7 @@ export function wrap(a: A): Decoder { */ export function apply( ua: Decoder, -): (ufai: Decoder I>) => Decoder { +): (ufai: Decoder I>) => Decoder { return FlatmappableDecoder.apply(ua); } diff --git a/deno.json b/deno.json index 3526709..91e0751 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,8 @@ { "name": "@baetheus/fun", - "version": "2.1.3", + "version": "2.2.0", "exports": { + "./ideas/dux": "./ideas/dux.ts", "./applicable": "./applicable.ts", "./array": "./array.ts", "./async": "./async.ts", @@ -22,6 +23,7 @@ "./fn": "./fn.ts", "./fn_either": "./fn_either.ts", "./foldable": "./foldable.ts", + "./free": "./free.ts", "./identity": "./identity.ts", "./initializable": "./initializable.ts", "./iterable": "./iterable.ts", @@ -45,17 +47,14 @@ "./showable": "./showable.ts", "./sortable": "./sortable.ts", "./state": "./state.ts", + "./stream": "./stream.ts", "./string": "./string.ts", "./sync": "./sync.ts", "./sync_either": "./sync_either.ts", "./these": "./these.ts", "./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" + "./wrappable": "./wrappable.ts" }, "include": [ "LICENSE", diff --git a/deno.lock b/deno.lock index 086574e..ef73d69 100644 --- a/deno.lock +++ b/deno.lock @@ -2,12 +2,27 @@ "version": "3", "packages": { "specifiers": { + "jsr:@std/assert@0.222.1": "jsr:@std/assert@0.222.1", + "jsr:@std/fmt@^0.222.1": "jsr:@std/fmt@0.222.1", "npm:@most/adapter@1.0.0": "npm:@most/adapter@1.0.0", "npm:@most/core@1.6.1": "npm:@most/core@1.6.1", "npm:@most/hold@4.1.0": "npm:@most/hold@4.1.0", "npm:@most/scheduler@1.3.0": "npm:@most/scheduler@1.3.0", "npm:@most/types@1.1.0": "npm:@most/types@1.1.0", - "npm:fast-check@3.14.0": "npm:fast-check@3.14.0" + "npm:effect@3.0.7": "npm:effect@3.0.7", + "npm:fast-check@3.14.0": "npm:fast-check@3.14.0", + "npm:rxjs@7.8.1": "npm:rxjs@7.8.1" + }, + "jsr": { + "@std/assert@0.222.1": { + "integrity": "691637161ee584a9919d1f9950ddd1272feb8e0a19e83aa5b7563cedaf73d74c", + "dependencies": [ + "jsr:@std/fmt@^0.222.1" + ] + }, + "@std/fmt@0.222.1": { + "integrity": "ec3382f9b0261c1ab1a5c804aa355d816515fa984cdd827ed32edfb187c0a722" + } }, "npm": { "@most/adapter@1.0.0": { @@ -55,6 +70,10 @@ "integrity": "sha512-v2trqAWu1jqP4Yd/CyI1O6mAeJyygK1uJOrFRpNPkPZIaYw4khA4EQe4WzcyOFKuXdiP8qAqaxGtXXJJ2LZdXg==", "dependencies": {} }, + "effect@3.0.7": { + "integrity": "sha512-VlEpWUc3IBc7k9NUdsdbh7cnGFNIdMpIoUFbbiVkt304d40pSr6Zadnr5Tk8m1cniqOLo4SXSZLw0M6f7N17Hg==", + "dependencies": {} + }, "fast-check@3.14.0": { "integrity": "sha512-9Z0zqASzDNjXBox/ileV/fd+4P+V/f3o4shM6QawvcdLFh8yjPG4h5BrHUZ8yzY6amKGDTAmRMyb/JZqe+dCgw==", "dependencies": { @@ -64,11 +83,22 @@ "pure-rand@6.0.4": { "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", "dependencies": {} + }, + "rxjs@7.8.1": { + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "tslib@2.6.2" + } + }, + "tslib@2.6.2": { + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dependencies": {} } } }, "redirects": { - "https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.146.0/testing/asserts.ts" + "https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.146.0/testing/asserts.ts", + "https://deno.land/x/fun/array.ts": "https://deno.land/x/fun@v2.0.0-alpha.9/array.ts" }, "remote": { "https://deno.land/std@0.103.0/fmt/colors.ts": "d2f8355f00a74404668fc5a1e4a92983ce1a9b0a6ac1d40efbd681cb8f519586", @@ -77,6 +107,44 @@ "https://deno.land/std@0.146.0/fmt/colors.ts": "6f9340b7fb8cc25a993a99e5efc56fe81bb5af284ff412129dd06df06f53c0b4", "https://deno.land/std@0.146.0/testing/_diff.ts": "029a00560b0d534bc0046f1bce4bd36b3b41ada3f2a3178c85686eb2ff5f1413", "https://deno.land/std@0.146.0/testing/_format.ts": "0d8dc79eab15b67cdc532826213bbe05bccfd276ca473a50a3fc7bbfb7260642", - "https://deno.land/std@0.146.0/testing/asserts.ts": "c17254748504f449c98476b78f4d206ca47cffc826e415026f09d7600c19535e" + "https://deno.land/std@0.146.0/testing/asserts.ts": "c17254748504f449c98476b78f4d206ca47cffc826e415026f09d7600c19535e", + "https://deno.land/x/fun@v2.0.0-alpha.9/alt.ts": "b61816a6a848b40eb40ec4548c4c00496dbc8df5f25d2e49fb35164fbd2d3ca3", + "https://deno.land/x/fun@v2.0.0-alpha.9/applicative.ts": "123483ae07379c29784a1a254b05c35ba50f3091f121a3257c03592d181bcb99", + "https://deno.land/x/fun@v2.0.0-alpha.9/apply.ts": "e2349f7b784525ecbad506325e106916c5290f1c65ef879dbf8ced5d3ed94858", + "https://deno.land/x/fun@v2.0.0-alpha.9/array.ts": "a9260d95f99debdae1a824cec23b7f2b4b71de36f2e485774b138b76ff076898", + "https://deno.land/x/fun@v2.0.0-alpha.9/bifunctor.ts": "514c39b7d64829b0f0623532aed1cb798bd87195b16f2998c11f0f13784347f0", + "https://deno.land/x/fun@v2.0.0-alpha.9/category.ts": "3bfe48095def1826d188d2574749bf02e419e3c9c21c95ac5c57ea93ef4d3cbf", + "https://deno.land/x/fun@v2.0.0-alpha.9/chain.ts": "73903ea7542ba84d71c8c77b68e51fad66dbc9dba66d98ebfe039888811f5cab", + "https://deno.land/x/fun@v2.0.0-alpha.9/comonad.ts": "7afff3dd7228143293ceb3324b18a5a2aebb90698f68199971557d3c8e272e09", + "https://deno.land/x/fun@v2.0.0-alpha.9/contravariant.ts": "cbab76a4b3f30e4f962ca0e261180f957dc3a6065ad8ef2d58430306ebf92817", + "https://deno.land/x/fun@v2.0.0-alpha.9/decode_error.ts": "80f45b8b9fae22cee7fb288b8ad5630c456daac681e1e171ba736b1c0b4fef24", + "https://deno.land/x/fun@v2.0.0-alpha.9/decoder.ts": "97680840ba7ffa464d3dff01fe1a4bf6aa2a13f7f39c4072e40a288ffda64207", + "https://deno.land/x/fun@v2.0.0-alpha.9/either.ts": "e9f649019a492ed92f38dfa5204c932368667276445a92b21f7d47a0e2f1dca1", + "https://deno.land/x/fun@v2.0.0-alpha.9/eq.ts": "6b5372adcd1604f099a60734704091f80bce66c8b77ce87b4d25d406260ae562", + "https://deno.land/x/fun@v2.0.0-alpha.9/extend.ts": "05716a9e32a6bad4730d0c2df77d762746832344071c050b570618fc881324d3", + "https://deno.land/x/fun@v2.0.0-alpha.9/filterable.ts": "e276588c8dce77054d60210e2d7a3a1d59b90bb859d697dc5387f1b7642c0a62", + "https://deno.land/x/fun@v2.0.0-alpha.9/fn.ts": "d735ad417984a033cff1d7503710c221054196b4bbe80dc05e85732dfded7c85", + "https://deno.land/x/fun@v2.0.0-alpha.9/fn_either.ts": "0ac2e9ce3489c79159a5511ae70e869dbb64c1260dd929a6e2f53f723e693ca3", + "https://deno.land/x/fun@v2.0.0-alpha.9/foldable.ts": "fd35ffe77b94b57f2a4dd9012f36890cd45340c3daa57a0fde31fe3b1b080ce1", + "https://deno.land/x/fun@v2.0.0-alpha.9/functor.ts": "9a6335138f79a7f7c5a83fdf8bc5ed7cc2018cf2be4bf3785a9b2b5f1103734c", + "https://deno.land/x/fun@v2.0.0-alpha.9/json_schema.ts": "c1cdbb8a05d683896bcb5f48ae612156313bc8dd809e03b97ec0b9eedc3a51e6", + "https://deno.land/x/fun@v2.0.0-alpha.9/kind.ts": "4f40ff5083c762bf8e93f2138d6d30fe77af71d030d7e2dcd5553b35c9afcdd8", + "https://deno.land/x/fun@v2.0.0-alpha.9/monad.ts": "85f9606c0db4a034e82511c13d99cc699267452cece3e9e63690f9cf1d738486", + "https://deno.land/x/fun@v2.0.0-alpha.9/monoid.ts": "0d0395fef0649dea804e0996beeebdc903d57bb09685cb6102e0d27b2984b0c9", + "https://deno.land/x/fun@v2.0.0-alpha.9/nilable.ts": "2b4d651ecff90c282a7959e2d731e456d76960bb9ea8cf42d3366963d3d29ece", + "https://deno.land/x/fun@v2.0.0-alpha.9/option.ts": "dc2e18065f956efa255b797a87d889567549bb790ffc2673852b83f46cc46f89", + "https://deno.land/x/fun@v2.0.0-alpha.9/ord.ts": "fefef7fe3b77752566aa395982ccea7a71fa8f99e6fe28f601c8bbeaf0772dcd", + "https://deno.land/x/fun@v2.0.0-alpha.9/pair.ts": "ff6e32a1922cea2a185936860486aa18058d8b43cc6c0171f09df193ac6e2e31", + "https://deno.land/x/fun@v2.0.0-alpha.9/predicate.ts": "e0bc4791efb36a0e2fed9436bb44d508af504e94efb87ceff9567108be9abd51", + "https://deno.land/x/fun@v2.0.0-alpha.9/profunctor.ts": "41b1a380f484bb4c80d1e68294f83bcc8957b6ae306361145a594feafc97706c", + "https://deno.land/x/fun@v2.0.0-alpha.9/record.ts": "6d21deadee621594533ba37e56cd84dc31693519cee408134a22af2c0e28dfe5", + "https://deno.land/x/fun@v2.0.0-alpha.9/refinement.ts": "6963ba7521edff6033b1dbd163d7c071b9ee4e312c845b890e138ea4a4a274d8", + "https://deno.land/x/fun@v2.0.0-alpha.9/schemable.ts": "d0cfff0c92e726169f64f685e039d92a552feb342dd745399ab66559ef978845", + "https://deno.land/x/fun@v2.0.0-alpha.9/semigroup.ts": "49d1fb0d5c46c0c57ce0e138b4e8ce8da7da2f0c298f467338772d34b5554ef1", + "https://deno.land/x/fun@v2.0.0-alpha.9/semigroupoid.ts": "c60020ed61cb4260c94030ee3a2e9c64c7993bdb75f3e25e14a5d9c1abe89042", + "https://deno.land/x/fun@v2.0.0-alpha.9/show.ts": "a14a323dab7981f1b1a59d9a6f6181ec9cb8ad3a71ae9f9bea8ae429145f6886", + "https://deno.land/x/fun@v2.0.0-alpha.9/state.ts": "1e31497366a29304af5baea17e195b753de9789b96787150a1b4b3d80755eac9", + "https://deno.land/x/fun@v2.0.0-alpha.9/traversable.ts": "932fa7238440ca1efbca4ff7ed8df1126c126018838c355a055edae60898e337", + "https://deno.land/x/fun@v2.0.0-alpha.9/tree.ts": "30304951ea946f6bf4326aa33414105afbe90b6e07126aa382b4147aa9119067" } } diff --git a/examples/store.ts b/examples/store.ts new file mode 100644 index 0000000..1d687ca --- /dev/null +++ b/examples/store.ts @@ -0,0 +1,92 @@ +import type { ReadonlyRecord } from "../record.ts"; +import type { DatumEither } from "../datum_either.ts"; + +import * as D from "../ideas/dux.ts"; +import * as O from "../optic.ts"; +import * as P from "../promise.ts"; +import * as S from "../stream.ts"; +import { pipe } from "../fn.ts"; + +const BOOKS: ReadonlyRecord = { + "a1": { id: "a1", author: "Tom Robbins", title: "Jitterbug Perfume" }, + "a2": { + id: "a2", + author: "Elizabeth Moon", + title: "The Deed of Paksenarrion", + }, +}; + +type Book = { + readonly id: string; + readonly author: string; + readonly title: string; +}; + +type MyState = { + readonly books: ReadonlyRecord>; +}; + +const INITIAL_STATE: MyState = { books: {} }; + +export const actionCreator = D.actionGroup("STORE"); + +export const getBookAction = actionCreator.async( + "GET_BOOK", +); + +export const getBookReducer = D.asyncCollectionRecord( + getBookAction, + pipe(O.id(), O.prop("books")), + (id) => + pipe( + O.id>>(), + O.atKey(id), + ), +); + +export const getBookEffect = D.onAction( + getBookAction.pending, + (id) => { + return pipe( + P.wait(Math.random() * 1000), + P.map(() => + id in BOOKS + ? getBookAction.success({ params: id, result: BOOKS[id] }) + : getBookAction.failure({ + params: id, + error: `Book with id ${id} does not exist`, + }) + ), + ); + }, +); + +const store = D.createStore(INITIAL_STATE, getBookReducer, getBookEffect); + +// State connection 1 +pipe( + store.state, + S.forEach((state) => console.log("OUTPUT 1", state)), +); + +// Optic to look at book at id a1 +const bookA1 = pipe( + O.id(), + O.prop("books"), + O.atKey("a1"), + O.some, + O.success, + O.props("title", "author"), +); + +// State connection 2: selecting book a1 +pipe( + store.state, + S.map(bookA1.view), + S.forEach((state) => console.log("OUTPUT 2", state)), +); + +// After a second, get book a1 +setTimeout(() => { + store.dispatch(getBookAction.pending("a1")); +}, 1000); diff --git a/flake.lock b/flake.lock index 8f77871..1aa6da9 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1709126324, - "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "d465f4819400de7c8d874d50b982301f28a84605", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1710066242, - "narHash": "sha256-bO7kahLdawW7rBqUTfWgf9mdPYrnOo5DGvWRJa9N8Do=", + "lastModified": 1715037484, + "narHash": "sha256-OUt8xQFmBU96Hmm4T9tOWTu4oCswCzoVl+pxSq/kiFc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "db339f1706f555794b71aa4eb26a5a240fb6a599", + "rev": "ad7efee13e0d216bf29992311536fce1d3eefbef", "type": "github" }, "original": { diff --git a/flatmappable.ts b/flatmappable.ts index d7153a6..93c58f3 100644 --- a/flatmappable.ts +++ b/flatmappable.ts @@ -13,10 +13,12 @@ import type { Applicable } from "./applicable.ts"; /** * A Flatmappable structure. */ -export interface Flatmappable extends Applicable, Hold { +export interface Flatmappable< + U extends Kind, +> extends Applicable, Hold { readonly flatmap: ( fati: (a: A) => $, - ) => ( + ) => ( ta: $, ) => $; } diff --git a/fn.ts b/fn.ts index 87b6f3f..0979aec 100644 --- a/fn.ts +++ b/fn.ts @@ -664,7 +664,7 @@ export function constant(a: A): () => A { */ export function apply( ua: Fn, -): (ufai: Fn I>) => Fn { +): (ufai: Fn I>) => Fn { return (ufai) => (d) => ufai(d)(ua(d)); } diff --git a/contrib/free.ts b/free.ts similarity index 95% rename from contrib/free.ts rename to free.ts index 047838e..0314f94 100644 --- a/contrib/free.ts +++ b/free.ts @@ -9,10 +9,10 @@ * @since 2.0.0 */ -import type { Kind, Out } from "../kind.ts"; -import type { Combinable } from "../combinable.ts"; +import type { Kind, Out } from "./kind.ts"; +import type { Combinable } from "./combinable.ts"; -import { flow, pipe } from "../fn.ts"; +import { flow, pipe } from "./fn.ts"; /** * @since 2.0.0 diff --git a/ideas/README.md b/ideas/README.md new file mode 100644 index 0000000..b67f618 --- /dev/null +++ b/ideas/README.md @@ -0,0 +1,10 @@ +# Fun Ideas + +This directory contains experimental ideas that fun may or may not implement. It +is not distributed via jsr or deno.land/x but can be referenced directly via +github branches. Occasionally, an idea will be moved into the root directory +with an @experimental tag in the module documentation. These modules are not api +stable and may change from time to time until stabilized. + +If you wish to add a new module to fun it's best to open a pull request with the +module in this directory so that merging it doesn't impact production use. diff --git a/ideas/callbag.ts b/ideas/callbag.ts new file mode 100644 index 0000000..24b7c3b --- /dev/null +++ b/ideas/callbag.ts @@ -0,0 +1,533 @@ +export type Talkback = (count: number) => void; + +export type Talk = { + readonly event: (value: A) => void; + readonly end: (reason?: unknown) => void; +}; + +export type Sink = (talkback: Talkback) => Talk; + +export type Stream = (sink: Sink, env: E) => Disposable; + +export type Timeout = { + readonly setTimeout: typeof setTimeout; + readonly clearTimeout: typeof clearTimeout; +}; + +export type Interval = { + readonly setInterval: typeof setInterval; + readonly clearInterval: typeof clearInterval; +}; + +export type TypeOf = S extends Stream ? A : never; + +export type EnvOf = S extends Stream ? E : never; + +export const Disposed = Symbol("fun/stream/disposed"); + +export function disposable(dispose: (reason?: unknown) => void): Disposable { + return { [Symbol.dispose]: dispose }; +} + +/** + * @todo: Consider how "safe" a sink needs to work. + */ +export function sink(snk: Sink): Sink { + return snk; + // return function safeSink(tlkbk) { + // let open = true; + // const talk = snk(tlkbk); + // return { + // event: (value) => { + // if (open) { + // return talk.event(value); + // } + // throw new Error("Talk event called after close."); + // }, + // end: (reason) => { + // if (open) { + // open = false; + // return talk.end(reason); + // } + // throw new Error("Talk end called after close."); + // }, + // }; + // }; +} + +export function stream(strm: Stream): Stream { + return strm; +} + +export function dispose(disposable: Disposable): void { + disposable[Symbol.dispose](); +} + +function NOOP(): void {} + +export function empty(): Stream { + return (snk) => { + let open = true; + const close = () => open = false; + const talk = snk(NOOP); + if (open) { + queueMicrotask(talk.end); + } + return disposable(close); + }; +} + +export function never(): Stream { + return (snk) => { + snk(NOOP); + return disposable(NOOP); + }; +} + +export function fromIterable(iterable: Iterable): Stream { + return function streamFromIterable(snk) { + let open = true; + let pulling = false; + let readyCount = 0; + const close = () => open = false; + + const talk = snk((count) => { + readyCount += count; + pull(); + }); + + function pull() { + if (open && !pulling) { + pulling = true; + if (readyCount > 0) { + for (const value of iterable) { + if (!open) { + break; + } + readyCount--; + talk.event(value); + } + } + if (readyCount > 0 && open) { + close(); + talk.end(); + } + pulling = false; + } + } + + return disposable(close); + }; +} + +export function fromPromise(p: Promise): Stream { + return (snk) => { + let open = true; + const close = () => open = false; + const talk = snk(() => + p.then((value) => { + if (open) { + close(); + talk.event(value); + talk.end(); + } + }) + ); + return disposable(close); + }; +} + +export function at(milliseconds: number): Stream { + return (snk, env) => { + let open = true; + let readyCount = 0; + + const close = () => open = false; + const { setTimeout, clearTimeout } = env; + const talk = snk((count) => { + readyCount += count; + }); + + const handle = setTimeout( + (talk) => { + if (open) { + if (readyCount > 0) { + talk.event(milliseconds); + } + close(); + talk.end(); + } + }, + milliseconds, + talk, + ); + + return disposable(() => { + clearTimeout(handle); + close(); + }); + }; +} + +export function periodic(period: number): Stream { + return (snk, env) => { + let open = true; + const close = () => open = false; + + let readyCount = 0; + const start = performance.now(); + const { setInterval, clearInterval } = env; + + const talk = snk((count) => { + readyCount += count; + }); + + const handle = setInterval( + (talk) => { + if (open && readyCount > 0) { + readyCount--; + talk.event(performance.now() - start); + } + }, + period, + talk, + ); + + return disposable(() => { + close(); + clearInterval(handle); + }); + }; +} + +export function range(count: number, start = 0, step = 1): Stream { + return (snk) => { + let open = true; + let index = Math.floor(Math.max(0, count)); + let value = start; + let readyCount = 0; + let pulling = false; + + const close = () => open = false; + const talk = snk((count) => { + readyCount += count; + pull(); + }); + + function pull() { + if (open && !pulling) { + pulling = true; + + // Empty range while open and readyCount not empty + while (open && readyCount > 0 && index > 0) { + talk.event(value); + value += step; + readyCount--; + index--; + } + + // Reached end of range without closing, close and end + if (open && readyCount > 0) { + close(); + talk.end(); + } + pulling = false; + } + } + + return disposable(close); + }; +} + +export function loop( + stepper: (state: S, value: A) => [S, B], + seed: S, +): (ua: Stream) => Stream { + return (ua) => (snk, env) => { + let state = seed; + return ua((tlkbk) => { + const talk = snk(tlkbk); + return { + event: (a) => { + const [next, b] = stepper(state, a); + state = next; + talk.event(b); + }, + end: talk.end, + }; + }, env); + }; +} + +export function wrap(value: A): Stream { + return (snk) => { + let open = true; + const close = () => open = false; + const talk = snk((count) => { + if (open && count > 0) { + close(); + talk.event(value); + } + }); + return disposable(close); + }; +} + +export function map( + fai: (a: A) => I, +): (ua: Stream) => Stream { + return loop((_, a) => [_, fai(a)], null); +} + +abstract class TransformTalk implements Talk { + constructor(protected readonly talk: Talk) {} + + abstract event(a: A): void; + + end(): void { + this.talk.end(); + } +} + +class ScanTalk extends TransformTalk { + constructor( + public readonly f: (b: B, a: A) => B, + private b: B, + talk: Talk, + ) { + super(talk); + } + + event(a: A) { + this.b = this.f(this.b, a); + this.talk.event(this.b); + } +} + +export function scan( + folder: (accumulator: O, value: A) => O, + initial: O, +): (ua: Stream) => Stream { + return (ua) => (snk, env) => + ua((tlk) => new ScanTalk(folder, initial, snk(tlk)), env); +} + +export function join( + concurrency = Number.POSITIVE_INFINITY, +): ( + ua: Stream, R2>, +) => Stream { + return (ua: Stream, R2>): Stream => + (snk: Sink, env: R1 & R2): Disposable => { + console.log("innerStream start", { concurrency, ua, snk, env }); + let readyCount = 0; + let outerOpen = true; + let index = 0; + const close = () => outerOpen = false; + const running = new Map(); + + /** + * When we receive a request for more data we broadcast to every running + * with the new count. As we receive events we broadcast negative numbers to + * each inner stream so we never send too many events down the pipe. + * + * This is naive and favors the first stream returned by the map forEach. A + * smarter version might keep track of each inner stream count locally. + */ + const talk = snk((count) => { + readyCount += count; + running.forEach(([tb]) => { + tb(count); + }); + console.log("outerTalkback", { count, readyCount, running }); + }); + + function createInnerStream(strm: Stream): void { + let tb: Talkback; + const streamId = index++; + const dsp = strm((innerTalkback) => { + tb = innerTalkback; + return { + event: (value) => { + console.log("innerStream event", { streamId, value }); + if (outerOpen) { + readyCount--; + talk.event(value); + running.forEach(([tb], id) => { + if (id !== streamId) { + tb(-1); + } + }); + } + }, + end: () => { + running.delete(streamId); + if (outerOpen && running.size < concurrency) { + talkback(1); + } + }, + }; + }, env); + + queueMicrotask(() => { + running.set(streamId, [tb, dsp]); + tb(readyCount); + }); + } + + let talkback: Talkback; + + const dsp = ua((outerTalkback) => { + console.log("outerStream start", { outerTalkback, running, readyCount }); + talkback = outerTalkback; + + return { + event: createInnerStream, + end: () => { + console.log("innerStream end", { running }); + close(); + running.forEach(([_, dsp]) => dispose(dsp)); + running.clear(); + }, + }; + }, env); + + queueMicrotask(() => talkback(concurrency)); + + return disposable(() => { + console.log("outerStream dispose"); + if (outerOpen) { + close(); + dispose(dsp); + } + }); + }; +} + +export function run(env: E): (ua: Stream) => Disposable { + return (ua) => + ua((tlkbk) => { + queueMicrotask(() => tlkbk(Number.POSITIVE_INFINITY)); + return { + event: NOOP, + end: NOOP, + }; + }, env); +} + +export function runPromise( + env: E, +): (stream: Stream) => Promise { + return (ua: Stream): Promise => + new Promise((resolve) => { + ua((tlk) => { + queueMicrotask(() => tlk(Number.POSITIVE_INFINITY)); + return { event: NOOP, end: () => resolve() }; + }, env); + }); +} + +export function collect( + env: E, + signal?: AbortSignal, +): (ua: Stream) => Promise> { + return (ua: Stream): Promise> => { + let open = true; + const close = () => open = false; + const values: Array = []; + + return new Promise((resolve, reject) => { + if (signal?.aborted) { + return reject(signal.reason); + } + + let talkback: Talkback; + + const dsp = ua((tlkbk) => { + talkback = tlkbk; + return { + event: (value) => { + if (open) { + values.push(value); + } + }, + end: () => { + if (open) { + close(); + resolve(values); + } + }, + }; + }, env); + + signal?.addEventListener("abort", () => { + if (open) { + close(); + dispose(dsp); + reject(signal.reason); + } + }); + + queueMicrotask(() => { + if (open) { + talkback(Number.POSITIVE_INFINITY); + } + }); + }); + }; +} + +export function forEach( + env: E, + onEvent: (value: A) => void, + onEnd: (reason?: unknown) => void, +): (ua: Stream) => void { + return (ua) => + ua((tlk) => { + queueMicrotask(() => tlk(Number.POSITIVE_INFINITY)); + return { event: onEvent, end: onEnd }; + }, env); +} + +export function take(count: number): (ua: Stream) => Stream { + return (ua) => (snk, env) => { + return ua((outerTalkback) => { + let remaining = count; + let readyCount = 0; + let open = true; + const close = () => open = false; + const talk = snk((outerCount) => { + const maxAdditional = Math.min(outerCount, count - readyCount); + readyCount += maxAdditional; + outerTalkback(maxAdditional); + }); + return { + event: (value) => { + if (open) { + remaining--; + talk.event(value); + if (remaining <= 0) { + close(); + talk.end(); + } + } + }, + end: (reason) => { + if (open) { + close(); + talk.end(reason); + } + }, + }; + }, env); + }; +} + +// import { pipe } from "../fn.ts"; + +// pipe( +// fromIterable([range(10), range(10, 100)]), +// join(), +// take(5), +// forEach({}, console.log, NOOP), +// ); diff --git a/ideas/dux.ts b/ideas/dux.ts new file mode 100644 index 0000000..1a94ade --- /dev/null +++ b/ideas/dux.ts @@ -0,0 +1,512 @@ +/** + * THe dux module is a mashup of ideas from redux, ngrx, flux, and flux standard + * actions. It provides a standard set of utilities for creating actions, + * reducers, and metareducers for use in any state management system as well as + * asynchronous effects and a store based on the kind stream implementation. + * + * This idea is currently highly experimental. While conceptually the basic + * concepts of actions, reducers, and effects are well defined, some + * implementation details around collections and effects are still unstable. Use + * at your own risk! + * + * @experimental + * @since 2.2.0 + * @module dux + */ +import type { $, Kind } from "../kind.ts"; +import type { Option } from "../option.ts"; +import type { DatumEither } from "../datum_either.ts"; +import type { Stream } from "../stream.ts"; +import type { Lens } from "../optic.ts"; +import type { KindReadonlyRecord } from "../record.ts"; +import type { KindReadonlyArray } from "../array.ts"; +import type { KindReadonlyMap } from "../map.ts"; +import type { KindReadonlySet } from "../set.ts"; + +import * as D from "../datum.ts"; +import * as DE from "../datum_either.ts"; +import * as O from "../option.ts"; +import * as S from "../stream.ts"; +import * as L from "../optic.ts"; +import { flow, pipe } from "../fn.ts"; + +/** + * 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; +} + +/** + * 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 ActionFactory

= + & ActionFunction

+ & ActionMatcher

+ & ActionType; + +/** + * Extract an Action type from an ActionFactory + * + * @since 2.1.0 + */ +export type ExtractAction = T extends ActionFactory[] ? 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 AsyncActionFactorys< + P, + R = unknown, + E = unknown, +> { + readonly pending: ActionFactory

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

(type: string) => ActionFactory

; + async: ( + type: string, + ) => AsyncActionFactorys; + group: G; +}; + +/** + * @since 2.1.0 + */ +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. + */ +function actionFunction

(type: string): ActionFunction

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

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

( + tag: string, +): ActionFactory

{ + return Object.assign( + actionFunction

(tag), + matcherFactory

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

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

(type: string) => actionFactory

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

, + reducer: Reducer, +): Reducer { + return (s, a) => (action.match(a) ? reducer(s, a.value) : s); +} + +/** + * Case function matches multiple ActionFactorys to a Reducer. + * + * @since 2.1.0 + */ +export function casesFn[]>( + actionCreators: A, + reducer: Reducer["value"]>, +): Reducer { + return (s, a) => + actionCreators.some(({ match }) => match(a)) + ? reducer(s, (> a).value) + : 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: AsyncActionFactorys, + lens: Lens>, +): Reducer { + return reducerFn( + caseFn(action.pending, lens.modify(D.toLoading)), + caseFn( + action.success, + (s: S, { result }) => lens.modify(() => DE.success(result))(s), + ), + caseFn( + action.failure, + (s: S, { error }) => lens.modify(() => DE.failure(error))(s), + ), + ); +} + +export function asyncCollectionFactory< + U extends Kind, +>(): ( + actionCreator: AsyncActionFactorys, + toCollection: Lens, B, C], [D], [E]>>, + toItemLens: ( + p: Payload, + ) => Lens< + $, B, C], [D], [E]>, + Option> + >, +) => Reducer { + return ({ pending, success, failure }, toCollection, toItemLens) => + reducerFn( + caseFn( + pending, + (s, p) => + pipe(toCollection, L.compose(toItemLens(p))).modify(flow( + O.map(D.toLoading), + O.alt(O.some(D.constPending())), + ))(s), + ), + caseFn( + success, + (s, { params, result }) => + pipe(toCollection, L.compose(toItemLens(params))).modify(() => + O.some(DE.success(result)) + )(s), + ), + caseFn( + failure, + (s, { params, error }) => + pipe(toCollection, L.compose(toItemLens(params))).modify(() => + O.some(DE.failure(error)) + )(s), + ), + ); +} + +type CollectionFactory = < + Payload, + Ok, + Err, + S, + B = never, + C = never, + D = unknown, + E = unknown, +>( + actionCreator: AsyncActionFactorys, + toCollection: Lens, B, C], [D], [E]>>, + toItemLens: ( + p: Payload, + ) => Lens< + $, B, C], [D], [E]>, + Option> + >, +) => Reducer; + +export const asyncCollectionRecord: CollectionFactory = + asyncCollectionFactory< + KindReadonlyRecord + >(); + +export const asyncCollectionArray: CollectionFactory = + asyncCollectionFactory(); + +export const asyncCollectionMap: CollectionFactory = + asyncCollectionFactory(); + +export const asyncCollectionSet: CollectionFactory = + asyncCollectionFactory(); + +/** + * Filters actions by first section of action type to bypass sections of the store + * + * @since 2.1.0 + */ +export const filterReducer = ( + match: string, + reducer: Reducer, +): Reducer => +(state, action) => + action.type.startsWith(match) ? reducer(state, action) : state; + +export type MetaReducer = ( + reducer: Reducer, +) => Reducer; + +export function metaReducerFn( + metareducer: MetaReducer, +): MetaReducer { + return metareducer; +} + +/** + * @since 2.1.2 + */ +export type Effect = ( + actions: Stream, + states: Stream, +) => Stream; + +/** + * @since 2.1.2 + */ +export function caseEff( + action: ActionFactory

, + effect: Effect, +): Effect { + return (actions, states) => + effect( + pipe( + actions, + S.filterMap((a) => action.match(a) ? O.some(a.value) : O.none), + ), + states, + ); +} + +/** + * @since 2.1.2 + */ +export function caseEffs[], R>( + actionCreators: A, + effect: Effect["value"], R>, +): Effect { + return (actions, states) => + effect( + pipe( + actions, + S.filterMap((a) => + actionCreators.some(({ match }) => match(a)) + ? O.some((> a).value) + : O.none + ), + ), + states, + ); +} + +/** + * @since 2.1.2 + */ +export function effectFn( + ...cases: ReadonlyArray> +): Effect { + return (s, a) => + pipe( + S.fromIterable(cases), + S.flatmap((eff) => eff(s, a)), + ); +} + +/** + * @since 2.2.0 + */ +export function onAction( + actionCreator: ActionFactory, + actionHandler: (payload: A) => void | ActionType | Promise, +): Effect { + return (actions) => + pipe( + actions, + S.filter(actionCreator.match), + S.flatmap(({ value }) => { + const action = actionHandler(value); + if (action === null || action === undefined) { + return S.empty(); + } else { + return S.fromPromise(Promise.resolve(action)); + } + }), + ); +} + +/** + * @since 2.1.2 + */ +export type Store = { + readonly state: Stream; + readonly dispatch: (action: ActionType) => void; +}; + +/** + * @since 2.2.0 + */ +export function createStore( + initial: State, + reducer: Reducer, +): Store; +export function createStore( + initial: State, + reducer: Reducer, + effect: Effect, +): Store; +export function createStore( + initial: State, + reducer: Reducer, + effect: Effect, + env: Env, +): Store; +export function createStore( + initial: State, + reducer: Reducer, + effect: Effect = () => S.empty(), + env: Env = S.DefaultEnv as Env, +): Store { + const [dispatch, action] = S.createAdapter(); + + const state = pipe( + S.wrap(initial), + S.combine(pipe( + action, + S.scan(reducer, initial), + )), + S.distinct(), + S.hold, + ); + + pipe( + effect(action, state), + S.forEach(dispatch, () => {}, env), + ); + + return { state, dispatch }; +} diff --git a/ideas/most2.ts b/ideas/most2.ts deleted file mode 100644 index 8b16ea7..0000000 --- a/ideas/most2.ts +++ /dev/null @@ -1,421 +0,0 @@ -import type { Intersect } from "../kind.ts"; -import { isNotNil } from "../nil.ts"; -import { pipe, todo } from "../fn.ts"; - -export type Stream = { - run(sink: Sink, env: R): Disposable; -}; - -export type Sink = { - event(a: A): void; - end(): void; -}; - -export type TypeOf = U extends Stream ? A : never; - -export type EnvOf = U extends Stream ? R : never; - -export function stream( - run: (sink: Sink, env: R) => Disposable, -): Stream { - return { run }; -} - -export function sink( - event: (a: A) => void, - end: () => void, -): Sink { - return { event, end }; -} - -export function disposable(dispose: () => void): Disposable { - return { [Symbol.dispose]: dispose }; -} - -export function dispose(disposable: Disposable): void { - disposable[Symbol.dispose](); -} - -export const DISPOSE_NONE: Disposable = disposable(() => undefined); - -export const EMPTY: Stream = stream( - (sink) => { - sink.end(); - return DISPOSE_NONE; - }, -); - -export const NEVER: Stream = stream(() => DISPOSE_NONE); - -export type Timeout = { - // deno-lint-ignore no-explicit-any - setTimeout( - f: (...a: Args) => void, - timeoutMillis: number, - ...a: Args - ): Disposable; -}; - -export function at(time: number): Stream { - return stream((sink: Sink, { setTimeout }: Timeout) => - setTimeout( - ({ event, end }) => { - event(undefined); - end(); - }, - time, - sink, - ) - ); -} - -export function continueWith( - second: () => Stream, -): (first: Stream) => Stream { - return (first) => - stream((snk, env) => { - let d = first.run( - sink(snk.event, () => { - d = second().run(snk, env); - }), - env, - ); - return d; - }); -} - -export function periodic(period: number): Stream { - return pipe( - at(period), - continueWith(() => periodic(period)), - ); -} - -export function map( - fai: (a: A) => I, -): (ua: Stream) => Stream { - return (ua) => - stream(({ event, end }, env) => - ua.run(sink((a) => event(fai(a)), end), env) - ); -} - -export function tap( - fa: (a: A) => void, -): (ua: Stream) => Stream { - return (ua) => - stream(({ event, end }, env) => - ua.run( - sink((a) => { - fa(a); - event(a); - }, end), - env, - ) - ); -} - -export function filter( - refinement: (a: A) => a is B, -): (s: Stream) => Stream; -export function filter( - predicate: (a: A) => boolean, -): (s: Stream) => Stream; -export function filter( - predicate: (a: A) => boolean, -): (ua: Stream) => Stream { - return (ua) => - stream(({ event, end }, env) => - ua.run( - sink((a) => { - if (predicate(a)) { - event(a); - } - }, end), - env, - ) - ); -} - -export function scan( - foao: (o: O, a: A) => O, - init: O, -): (ua: Stream) => Stream { - return (ua) => - stream(({ event, end }, env) => { - let hold = init; - return ua.run( - sink((a) => { - hold = foao(hold, a); - event(hold); - }, end), - env, - ); - }); -} - -// deno-lint-ignore no-explicit-any -export function merge[]>( - ...streams: Streams -): Stream< - TypeOf, - Intersect>[0]> -> { - return stream(({ event, end }, env) => { - let count = streams.length; - const disposables: Disposable[] = []; - const done = () => disposables.forEach(dispose); - - // last stream disposes and pass an end to the sink - const snk = sink(event, () => { - if (--count === 0) { - done(); - end(); - } - }); - - // dump the merged disposables into our stateful array - disposables.push(...streams.map((s) => s.run(snk, env))); - return disposable(done); - }); -} - -// deno-lint-ignore no-explicit-any -export function combine[]>( - init: { readonly [K in keyof Streams]: TypeOf }, - ...streams: Streams -): Stream< - { readonly [K in keyof Streams]: TypeOf }, - Intersect>[0]> -> { - return stream(({ event, end }, env) => { - let count = streams.length; - const values = [...init] as { [K in keyof Streams]: TypeOf }; - const disposables: Disposable[] = []; - const done = () => disposables.forEach(dispose); - - disposables.push( - ...streams.map((s, i) => - s.run( - sink((a: TypeOf) => { - values[i] = a; - event(values); - }, () => { - if (--count === 0) { - done(); - end(); - } - }), - env, - ) - ), - ); - - return disposable(done); - }); -} - -export type SeedValue = { readonly seed: S; readonly value: V }; - -export function loop( - stepper: (seed: S, a: A) => SeedValue, - seed: S, -): (ua: Stream) => Stream { - return (ua) => - stream(({ event, end }, env) => { - let hold: S = seed; - return ua.run( - sink((a) => { - const { seed, value } = stepper(hold, a); - hold = seed; - event(value); - }, end), - env, - ); - }); -} - -export function join(concurrency = 1): ( - ua: Stream, R2>, -) => Stream { - return ( - ua: Stream, R2>, - ): Stream => - stream(({ event, end }, env) => { - let done = false; - const queue: Stream[] = []; - const running = new Map, Disposable>(); // WeakMap? - - function startInner(strm: Stream) { - const snk = sink(event, () => { - if (running.has(snk)) { - running.delete(snk); - } - - if (queue.length > 0 && running.size < concurrency) { - startInner(queue.shift() as Stream); - } - - if (done && queue.length === 0 && running.size === 0) { - end(); - } - }); - - running.set(snk, strm.run(snk, env)); - } - - return ua.run( - sink((strm) => { - if (running.size < concurrency) { - startInner(strm); - } else { - queue.push(strm); - } - }, () => { - done = true; - }), - env, - ); - }); -} - -export function takeUntil( - predicate: (a: A) => boolean, -): (ua: Stream) => Stream { - return (ua) => - stream(({ event, end }, env) => { - const dsp = ua.run( - sink((a) => { - if (predicate(a)) { - end(); - } else { - event(a); - } - }, end), - env, - ); - return dsp; - }); -} - -// export function flatMap( -// faui: (a: A) => Stream, -// count = 1, -// ): (ua: Stream) => Stream { -// return (ua) => -// stream(({ event, end }, env) => { -// const queue = new Array>(); -// const running = new Map, Disposable>(); - -// const = () => { -// const snk = sink(event, () => { -// const disposable = running.get(snk); -// if (isNotNil(disposable)) { -// dispose(disposable); -// running.delete(snk); -// } -// }); - -// }; - -// ua.run( -// sink((a) => { -// const strm = faui(a); - -// if (running.size < count) { -// const snk = sink(event, () => { -// const disposable = running.get(snk); -// if (isNotNil(disposable)) { -// dispose(disposable); -// running.delete(snk); -// } - -// }); -// } -// }, () => {}), -// env, -// ); -// }); -// } - -export function indexed( - fsi: (s: S) => [I, S], - init: S, -): (ua: Stream) => Stream<[I, A]> { - return loop((s, a) => { - const [index, seed] = fsi(s); - return { seed, value: [index, a] }; - }, init); -} - -export function withIndex( - start: number = 0, - step: number = 1, -): (ua: Stream) => Stream<[number, A], R> { - return indexed((i) => [i, i + step], start); -} - -export function withCount( - ua: Stream, -): Stream<[number, A], R> { - return pipe(ua, withIndex(1)); -} - -export function createAdapter(): [ - (value: A) => void, - Stream, -] { - const dispatcher = { dispatch: (_: A) => {} }; - const dispatch = (a: A) => dispatcher.dispatch(a); - return [ - dispatch, - stream(({ event, end }) => { - dispatcher.dispatch = event; - return disposable(() => { - dispatcher.dispatch = () => {}; - end(); - }); - }), - ]; -} - -export function fromPromise(ua: Promise): Stream { - return stream((sink) => { - const dispatcher = { dispatch: sink.event }; - const done = disposable(() => { - dispatcher.dispatch = () => {}; - }); - ua.then((a) => dispatcher.dispatch(a)).finally(() => dispose(done)); - return done; - }); -} - -export function run( - sink: Sink, - env: R, -): (ua: Stream) => Disposable { - return (ua) => ua.run(sink, env); -} - -export function runPromise(env: R): (ua: Stream) => Promise { - return (ua) => - new Promise((resolve) => run(sink(() => {}, resolve), env)(ua)); -} - -const test = pipe( - periodic(1000), - withCount, - map(([i]) => - pipe( - periodic(200), - withCount, - map(([j]): [number, number] => [i, j]), - takeUntil(([_, j]) => j > 3), - ) - ), - join(Number.POSITIVE_INFINITY), -); - -const timeout: Timeout = { setTimeout } as unknown as Timeout; - -pipe(test, run(sink(console.log, () => console.log("done")), timeout)); diff --git a/identity.ts b/identity.ts index 2008d09..d36de70 100644 --- a/identity.ts +++ b/identity.ts @@ -14,6 +14,7 @@ import type { Flatmappable } from "./flatmappable.ts"; import type { Wrappable } from "./wrappable.ts"; /** + * Represents the Identity type constructor. * @since 2.0.0 */ export type Identity = A; @@ -26,6 +27,23 @@ export interface KindIdentity extends Kind { } /** + * Wraps a value into an Identity type. + * This function allows any value to be lifted into the context of an Identity, + * making it possible to interact with other functions that operate on the Identity type. + * + * @example + * ```ts + * import { wrap } from "./identity.ts"; + * + * // numberIdentity is Identity with value 5 + * const numberIdentity = wrap(5); + * + * // stringIdentity is Identity with value "hello" + * const stringIdentity = wrap("hello"); + * ``` + * + * @param a - The value to wrap. + * @returns The wrapped value as an Identity. * @since 2.0.0 */ export function wrap(a: A): Identity { @@ -35,9 +53,7 @@ export function wrap(a: A): Identity { /** * @since 2.0.0 */ -export function map( - fai: (a: A) => I, -): (ta: Identity) => Identity { +export function map(fai: (a: A) => I): (ta: Identity) => Identity { return fai; } diff --git a/kind.ts b/kind.ts index 0bb93b5..e1680e7 100644 --- a/kind.ts +++ b/kind.ts @@ -95,17 +95,19 @@ export type $< /** * Access the Covariant substitution type at index N */ -export type Out = T["out"][N]; +export type Out = + T["out"][N]; /** * Access the Premappable substitution type at index N */ -export type In = T["in"][N]; +export type In = T["in"][N]; /** * Access the Invariant substitution type at index N */ -export type InOut = T["inout"][N]; +export type InOut = + T["inout"][N]; /** * Fix a concrete type as a non-substituting Kind. This allows one to define diff --git a/optic.ts b/optic.ts index 8e13646..fe4d754 100644 --- a/optic.ts +++ b/optic.ts @@ -82,13 +82,25 @@ export type FoldTag = typeof FoldTag; */ export type Tag = LensTag | AffineTag | FoldTag; +export const YesRev = "YesRev" as const; +export type YesRev = typeof YesRev; + +export const NoRev = "NoRev" as const; +export type NoRev = typeof NoRev; + +export type Rev = YesRev | NoRev; + +export type AlignRev = A extends YesRev + ? B extends YesRev ? YesRev : NoRev + : NoRev; + /** * A type level mapping from an Optic Tag to its associated output Kind. This is * used to substitute the container of the output of a view function. */ type ToKind = T extends LensTag ? I.KindIdentity : T extends AffineTag ? O.KindOption - : T extends FoldTag ? A.KindArray + : T extends FoldTag ? A.KindReadonlyArray : never; /** * A type level computation of Optic Tags. When composing the view functions of @@ -129,7 +141,6 @@ function align( * * AffineTag => AffineTag * * AffineTag => FoldTag * * FoldTag => FoldTag - * * The following are unsupported casts which will throw at runtime: * * * AffineTag => LensTag @@ -140,30 +151,30 @@ function align( * to extend its functionality by replicating the cast logic, these cases must * be considered. */ -export function _unsafeCast( - viewer: Viewer, +export function _unsafeCast( + optic: Optic, tag: V, -): Viewer["view"] { - type Out = Viewer["view"]; +): Optic["view"] { + type Out = Optic["view"]; // Covers Lens => Lens, AffineFold => AffineFold, Fold => Fold - if (viewer.tag === tag as LensTag) { - return viewer.view as Out; + if (optic.tag === tag as LensTag) { + return optic.view as Out; // AffineFold => Fold - } else if (tag === FoldTag && viewer.tag === AffineTag) { + } else if (tag === FoldTag && optic.tag === AffineTag) { return (s: S) => { - const ua = viewer.view(s) as Option; + const ua = optic.view(s) as Option; return (O.isNone(ua) ? [] : [ua.value]) as ReturnType; }; // Lens => Fold - } else if (tag === FoldTag && viewer.tag === LensTag) { - return (s: S) => [viewer.view(s)] as ReturnType; + } else if (tag === FoldTag && optic.tag === LensTag) { + return (s: S) => [optic.view(s)] as ReturnType; // Lens => AffineFold - } else if (tag === AffineTag && viewer.tag == LensTag) { - return (s) => O.wrap(viewer.view(s)) as ReturnType; + } else if (tag === AffineTag && optic.tag == LensTag) { + return (s) => O.wrap(optic.view(s)) as ReturnType; } // Non-valid casts will throw an error at runtime. // This is not reachable with the combinators in this lib. - throw new Error(`Attempted to cast ${viewer.tag} to ${tag}`); + throw new Error(`Attempted to cast ${optic.tag} to ${tag}`); } /** @@ -188,49 +199,29 @@ function getFlatmappable(tag: T): Flatmappable> { } /** - * A Viewer implements a view function `(s: S) => T`. This is - * effectively a Kliesli Arrow. The valid types of T are Identity, Option, and - * Array. Viewer also includes a runtime tag corresponding to the return type of - * the view function to aid in composition. + * An Optic is defined as a Viewer combined with a Modifier. This is the root type for the specific types of Optics defined below. * * @since 2.0.0 */ -export interface Viewer { +export interface Optic { readonly tag: T; readonly view: (s: S) => $, [A, never, never]>; -} - -/** - * The Modifier type implements the modify function - * `(mod: (a: A) => A) => (s: S) => S`. This type is directly composable and - * from it one can recover set/replace behavior. - * - * @since 2.0.0 - */ -export interface Modifier { readonly modify: (modifyFn: (a: A) => A) => (s: S) => S; + readonly review: R extends YesRev ? (a: A) => S : never; } -/** - * The Reviewer S`. This - * type is directly composable and is used when the S type in Viewer - * can be reconstructed from A. Some examples are constructing `Option` - * from `number`, `Array` from `number`, etc. - * - * @since 2.0.0 - */ -export interface Reviewer { - readonly review: (a: A) => S; -} +export type TagOf = U extends Optic + ? T + : never; -/** - * An Optic is defined as a Viewer combined with a Modifier. This is the root type for the specific types of Optics defined below. - * - * @since 2.0.0 - */ -export interface Optic - extends Viewer, Modifier {} +export type ViewOf = U extends Optic + ? S + : never; + +export type TypeOf = U extends Optic + ? A + : never; /** * Lens is an alias of Optic. This means that the view @@ -249,7 +240,7 @@ export type Lens = Optic; * * @since 2.0.0 */ -export type Iso = Lens & Reviewer; +export type Iso = Optic; /** * AffineFold is an alias of Optic. This means the view @@ -272,7 +263,7 @@ export type AffineFold = Optic; * * @since 2.0.0 */ -export type Prism = AffineFold & Reviewer; +export type Prism = Optic; /** * Fold is an alias of Optic. This means that the view @@ -293,96 +284,7 @@ export type Fold = Optic; * * @since 2.0.0 */ -export type Refold = Fold & Reviewer; - -/** - * Construct a Viewer from a tag T and a view function that matches - * said tag. This is a raw constructor and is generally only useful if there is - * a case where a structure can be lensed into but not reconstructed or - * traversed. However, this is the core composable structure of optics, as they - * are primarily meant as a way to retrieve data from an existing structure. - * - * @example The "Lens" viewer retrieves a single value that always exists. - * ```ts - * import type { Pair } from "./pair.ts"; - * - * import * as O from "./optic.ts"; - * import * as P from "./pair.ts"; - * - * const fst = () => O.viewer, A>( - * O.LensTag, - * P.getFirst, - * ); - * - * const numPair = fst(); - * - * const result1 = numPair.view(P.pair(1, 2)); // 1 - * const result2 = numPair.view(P.pair(2, 1)); // 2 - * ``` - * - * @example The "Affine" viewer retrieves a single value that might exists. - * ```ts - * import type { Either } from "./either.ts"; - * - * import * as O from "./optic.ts"; - * import * as E from "./either.ts"; - * - * const right = () => O.viewer, R>( - * O.AffineTag, - * E.getRight, - * ); - * - * const numberEither = right(); - * - * const result1 = numberEither.view(E.right(1)); // Some(1) - * const result2 = numberEither.view(E.left("Hello")); // None - * ``` - * - * @example The "Fold" viewer retrieves zero or more values as an Array. - * ```ts - * import * as O from "./optic.ts"; - * - * const record = () => O.viewer, A>( - * O.FoldTag, - * Object.values, - * ); - * - * const numberRecord = record(); - * - * const result = numberRecord.view({ - * "one": 1, - * "two": 2, - * }); // [1, 2] - * ``` - * - * @since 2.0.0 - */ -export function viewer( - tag: T, - view: (s: S) => $, [A, never, never]>, -): Viewer { - return { tag, view }; -} - -/** - * Construct a Modifier( - modify: (modifyFn: (a: A) => A) => (s: S) => S, -): Modifier { - return { modify }; -} - -/** - * Construct a Reviewer from a review function. - * - * @since 2.0.0 - */ -export function reviewer(review: (a: A) => S): Reviewer { - return { review }; -} +export type Refold = Optic; /** * Construct an Optic & Reviewer from a tag as well as view, @@ -390,31 +292,15 @@ export function reviewer(review: (a: A) => S): Reviewer { * * @since 2.0.0 */ -export function optic( - tag: U, - view: (s: S) => $, [A, never, never]>, - modify: (modifyFn: (a: A) => A) => (s: S) => S, - review: (a: A) => S, -): Optic & Reviewer; -/** - * Construct an Optic from a tag as well as view and modify functions. - * - * @since 2.0.0 - */ -export function optic( - tag: U, - view: (s: S) => $, [A, never, never]>, - modify: (modifyFn: (a: A) => A) => (s: S) => S, -): Optic; -export function optic( +export function optic( tag: U, view: (s: S) => $, [A, never, never]>, modify: (modifyFn: (a: A) => A) => (s: S) => S, review?: (a: A) => S, -): Optic | Optic & Reviewer { - return typeof review === "function" +): Optic { + return (typeof review === "function" ? { tag, view, modify, review } - : { tag, view, modify }; + : { tag, view, modify }) as Optic; } /** @@ -662,114 +548,6 @@ export function fromPredicate(predicate: Predicate): Prism { return prism(O.fromPredicate(predicate), identity); } -/** - * A pipeable view function that applies a value S to a Viewer. It will - * return either a raw value, an option, or a readonlyarray based on the tag of - * the Viewer. Note: All Optics are Viewers. - * - * @example - * ```ts - * import * as O from "./optic.ts"; - * import { pipe } from "./fn.ts"; - * - * type Foo = { readonly bar: number }; - * - * const bar = pipe(O.id(), O.prop("bar")); - * - * const result = pipe(bar, O.view({ bar: 1 })); // 1 - * ``` - * - * @since 2.0.0 - */ -export function view( - s: S, -): ( - viewer: Viewer, -) => ReturnType { - return (viewer) => viewer.view(s); -} - -/** - * A pipeable modify function that applies a modification function to a - * Modifier modify function. It will return a function S -> S that applies - * the modify function according to the type of optic. Note: All Optics are - * Modifiers. - * - * @example - * ```ts - * import * as O from "./optic.ts"; - * import { pipe } from "./fn.ts"; - * - * type Person = { readonly name: string }; - * - * const name = pipe(O.id(), O.prop("name")); - * - * const upper = pipe(name, O.modify(s => s.toUpperCase())); - * - * const result1 = upper({ name: "brandon" }); // { name: "BRANDON" } - * ``` - * - * @since 2.0.0 - */ -export function modify(faa: (a: A) => A): ( - modifier: Modifier, -) => ReturnType { - return (modifier) => modifier.modify(faa); -} - -/** - * A pipeable replace function, that uses the modify function of an Optic to - * replace an existing value over the structure S. - * - * @example - * ```ts - * import * as O from "./optic.ts"; - * import { pipe } from "./fn.ts"; - * - * type Person = { name: string }; - * - * const name = pipe(O.id(), O.prop("name")); - * const toBrandon = pipe(name, O.replace("Brandon")); - * - * const tina: Person = { name: "Tina" } - * - * const result = toBrandon(tina); // { name: "Brandon" } - * ``` - * - * @since 2.0.0 - */ -export function replace(a: A): ( - modifier: Modifier, -) => (s: S) => S { - const value = () => a; - return (modifier) => modifier.modify(value); -} - -/** - * A pipeable review function that applies a value A to the the review function - * of a Reviewer. It returns a value S. - * - * @example - * ```ts - * import * as O from "./optic.ts"; - * import * as S from "./set.ts"; - * import { pipe } from "./fn.ts"; - * - * const numberSet = O.refold, number>( - * Array.from, - * S.wrap, - * S.map, - * ); - * - * const result = pipe(numberSet, O.review(1)); // ReadonlySet(1) - * ``` - * - * @since 2.0.0 - */ -export function review(a: A): (reviewer: Reviewer) => S { - return (reviewer) => reviewer.review(a); -} - /** * All id functions for Optics are satisfied by iso(identity, identity). Thus, * we construct a singleton to cut down on computation. Generally, this function @@ -800,6 +578,12 @@ export function id(): Iso { return _identity; } +function hasReview( + optic: Optic, +): optic is Optic { + return Object.hasOwn(optic, "review") && typeof optic.review === "function"; +} + /** * Compose two optics, aligning their tag and building the composition using * natural transformations and monadic chaining for the view function and using @@ -826,11 +610,11 @@ export function id(): Iso { * even, * O.compose(positive), * ); - * const addTwo = pipe(evenPos, O.modify(n => n + 2)); + * const addTwo = evenPos.modify(n => n + 2); * - * const result1 = pipe(evenPos, O.view(0)); // None - * const result2 = pipe(evenPos, O.view(1)); // None - * const result3 = pipe(evenPos, O.view(2)); // Some(2) + * const result1 = evenPos.view(0); // None + * const result2 = evenPos.view(1); // None + * const result3 = evenPos.view(2); // Some(2) * const result4 = addTwo(0); // 0 * const result5 = addTwo(1); // 1 * const result6 = addTwo(2); // 2 @@ -838,14 +622,14 @@ export function id(): Iso { * * @since 2.0.0 */ -export function compose( - second: Optic, -): ( - first: Optic, -) => Optic, S, I> { - return ( - first: Optic, - ): Optic, S, I> => { +export function compose( + second: Optic, +): ( + first: Optic, +) => Optic, S, I, AlignRev> { + return ( + first: Optic, + ): Optic, S, I, AlignRev> => { const tag = align(first.tag, second.tag); const _chain = getFlatmappable(tag).flatmap; const _first = _unsafeCast(first, tag); @@ -854,42 +638,15 @@ export function compose( const view = flow(_first, _chain(_second)); const modify = flow(second.modify, first.modify); + if (hasReview(first) && hasReview(second)) { + const review = flow(second.review, first.review); + return optic(tag, view, modify, review); + } + return optic(tag, view, modify); }; } -/** - * Compose two reviewer functions, allowing one to create nested Reviewer - * structures. - * - * @example - * ```ts - * import * as O from "./optic.ts"; - * import * as S from "./set.ts"; - * import { pipe } from "./fn.ts"; - * - * const set = () => O.refold, A>( - * Array.from, - * S.wrap, - * S.map, - * ); - * - * const sets = pipe( - * set>(), - * O.composeReviewer(set()), - * ); - * - * const result = sets.review(1); // Set(Set(1)) - * ``` - * - * @since 2.0.0 - */ -export function composeReviewer( - second: Reviewer, -): (first: Reviewer) => Reviewer { - return (first) => reviewer((i) => first.review(second.review(i))); -} - /** * Construct a Lens Viewer from a raw value A. The view function of this viewer * operatates like constant(a). @@ -906,8 +663,8 @@ export function composeReviewer( * * @since 2.0.0 */ -export function wrap(a: A): Viewer { - return viewer(LensTag, (_: S) => a); +export function wrap(a: A): Iso { + return iso(() => a, identity, (faa) => faa); } /** @@ -933,9 +690,9 @@ export function wrap(a: A): Viewer { export function imap( fai: (a: A) => I, fia: (i: I) => A, -): ( - first: Optic, -) => Optic, S, I> { +): ( + first: Optic, +) => Optic, S, I, AlignRev> { return compose(iso(fai, fia)); } @@ -957,9 +714,9 @@ export function imap( * const brandon: Person = { name: "Brandon", age: 37 }; * const emily: Person = { name: "Emily", age: 35 }; * - * const result1 = pipe(name, O.view(brandon)); // "Brandon" - * const result2 = pipe(name, O.view(emily)); // "Emily" - * const result3 = pipe(age, O.view(brandon)); // 37 + * const result1 = name.view(brandon); // "Brandon" + * const result2 = name.view(emily); // "Emily" + * const result3 = age.view(brandon); // 37 * const result4 = pipe(brandon, name.modify(toUpperCase)); * // { name: "BRANDON", age: 37 } * ``` @@ -968,7 +725,9 @@ export function imap( */ export function prop( prop: P, -): (sa: Optic) => Optic, S, A[P]> { +): ( + sa: Optic, +) => Optic, S, A[P]> { return compose(lens((s: A) => s[prop], (fii) => (a) => { const out = fii(a[prop]); return a[prop] === out ? a : { ...a, [prop]: out }; @@ -999,16 +758,16 @@ export function prop( * published: new Date("May 01 1979"), * }; * - * const result1 = pipe(short, O.view(suttree)); + * const result1 = short.view(suttree); * // { title: "Suttree", description: "Cormac on Cormac" } * ``` * * @since 2.0.0 */ -export function props, P extends keyof A>( +export function props, P extends keyof A>( ...props: [P, P, ...Array

] -): ( - first: Optic, +): ( + first: Optic, ) => Optic, S, { [K in P]: A[K] }> { const pick = R.pick(...props); return compose(lens( @@ -1036,16 +795,16 @@ export function props, P extends keyof A>( * O.index(1), * ); * - * const result1 = pipe(second, O.view([])); // None - * const result2 = pipe(second, O.view(["Hello", "World"])); // Some("World") + * const result1 = second.view([]); // None + * const result2 = second.view(["Hello", "World"]); // Some("World") * ``` * * @since 2.0.0 */ export function index( index: number, -): ( - first: Optic>, +): ( + first: Optic, R>, ) => Optic, S, A> { return compose(affineFold(A.lookup(index), A.modifyAt(index))); } @@ -1063,16 +822,16 @@ export function index( * O.key("one"), * ); * - * const result1 = pipe(one, O.view({})); // None - * const result2 = pipe(one, O.view({ one: "one" })); // Some("one") + * const result1 = one.view({}); // None + * const result2 = one.view({ one: "one" }); // Some("one") * ``` * * @since 2.0.0 */ export function key( key: string, -): ( - first: Optic>, +): ( + first: Optic, R>, ) => Optic, S, A> { return compose(affineFold(R.lookupAt(key), R.modifyAt(key))); } @@ -1092,10 +851,10 @@ export function key( * O.id>>(), * O.atKey("one"), * ); - * const removeAtOne = pipe(atOne, O.replace(constNone())); + * const removeAtOne = atOne.modify(constNone); * - * const result1 = pipe(atOne, O.view({})); // None - * const result2 = pipe(atOne, O.view({ one: "one" })); // Some("one") + * const result1 = atOne.view({}); // None + * const result2 = atOne.view({ one: "one" }); // Some("one") * const result3 = removeAtOne({}); // {} * const result4 = removeAtOne({ one: "one" }); // {} * const result5 = removeAtOne({ one: "one", two: "two" }); // { two: "two" } @@ -1105,8 +864,8 @@ export function key( */ export function atKey( key: string, -): ( - first: Optic>>, +): ( + first: Optic>, R>, ) => Optic, S, Option> { const lookup = R.lookupAt(key); const _deleteAt = R.deleteAt(key); @@ -1132,8 +891,8 @@ export function atKey( * * const positive = pipe(O.id(), O.filter(n => n > 0)); * - * const result1 = pipe(positive, O.view(1)); // Some(1); - * const result2 = pipe(positive, O.view(0)); // None + * const result1 = positive.view(1); // Some(1); + * const result2 = positive.view(0); // None * const result3 = pipe(1, positive.modify(n => n + 1)); // 2 * const result4 = pipe(0, positive.modify(n => n + 1)); // 0 * ``` @@ -1142,19 +901,19 @@ export function atKey( */ export function filter( r: Refinement, -): ( - first: Optic, -) => Optic, S, B>; +): ( + first: Optic, +) => Optic, S, B, AlignRev>; export function filter( r: Predicate, -): ( - first: Optic, -) => Optic, S, A>; +): ( + first: Optic, +) => Optic, S, A, AlignRev>; export function filter( predicate: Predicate, -): ( - first: Optic, -) => Optic, S, A> { +): ( + first: Optic, +) => Optic, S, A, AlignRev> { return compose(fromPredicate(predicate)); } @@ -1177,18 +936,20 @@ export function filter( * const insensitive = pipe(ComparableString, premap(toLowerCase)); * * const fun = pipe(O.id(), O.atMap(insensitive)("fun")); - * const remove = pipe(fun, O.replace(constNone())); + * const remove = fun.modify(constNone); * - * const result1 = pipe(fun, O.view(new Map([["FUN", 100]]))); // Some(100) - * const result2 = pipe(fun, O.view(M.init())); // None - * const result3 = remove(new Map([["FUN", 100], ["not", 10]])); + * const result1 = fun.view(M.readonlyMap(["FUN", 100])); // Some(100) + * const result2 = fun.view(M.init()); // None + * const result3 = remove(M.readonlyMap(["FUN", 100], ["not", 10])); * // Map("not": 10); * ``` * * @since 2.0.0 */ -export function atMap(eq: Comparable): (key: B) => ( - first: Optic>, +export function atMap( + eq: Comparable, +): (key: B) => ( + first: Optic, R>, ) => Optic, S, Option> { return (key) => { const lookup = M.lookup(eq)(key); @@ -1223,8 +984,8 @@ export function atMap(eq: Comparable): (key: B) => ( * const tree1: Data = { tree: T.tree(1, [T.tree(2), T.tree(3)]) }; * const tree2: Data = { tree: T.tree(0) }; * - * const result1 = pipe(numbers, O.view(tree1)); // [1, 2, 3] - * const result2 = pipe(numbers, O.view(tree2)); // [0] + * const result1 = numbers.view(tree1); // [1, 2, 3] + * const result2 = numbers.view(tree2); // [0] * const result3 = pipe(tree1, numbers.modify(n => n + 1)); * // Tree(2, [Tree(3), Tree(4)]) * ``` @@ -1233,8 +994,8 @@ export function atMap(eq: Comparable): (key: B) => ( */ export function traverse( T: Traversable, -): ( - first: Optic>, +): ( + first: Optic, R>, ) => Optic, S, A> { return compose(fold( T.fold((as, a) => [...as, a], ((): A[] => [])()), @@ -1278,9 +1039,11 @@ export function traverse( export function combineAll( initializable: Initializable, fai: (a: A) => I, -): (first: Optic) => (s: S) => I { +): (first: Optic) => (s: S) => I { const _combineAll = getCombineAll(initializable); - return (first: Optic): (s: S) => I => { + return ( + first: Optic, + ): (s: S) => I => { const view = _unsafeCast(first, FoldTag); return flow(view, A.map(fai), (is) => _combineAll(...is)); }; @@ -1294,17 +1057,18 @@ export function combineAll( * import * as O from "./optic.ts"; * import { pipe } from "./fn.ts"; * - * const result = pipe( + * const nums = pipe( * O.id>>(), * O.record, - * O.view({ one: 1, two: 2 }), - * ); // [1, 2] + * ); + * + * const result = nums.view({ one: 1, two: 2 }); // [1, 2] * ``` * * @since 2.0.0 */ export const record: ( - first: Optic>, + first: Optic, Rev>, ) => Optic, S, A> = traverse(R.TraversableRecord); /** @@ -1315,18 +1079,18 @@ export const record: ( * import * as O from "./optic.ts"; * import { pipe } from "./fn.ts"; * - * const result = pipe( + * const filtered = pipe( * O.id>(), * O.array, * O.filter(n => n % 2 === 0), - * O.view([1, 2, 3]), - * ); // [2] + * ); + * const result = filtered.view([1, 2, 3]); // [2] * ``` * * @since 2.0.0 */ -export const array: ( - first: Optic>, +export const array: ( + first: Optic, R>, ) => Optic, S, A> = traverse(A.TraversableArray); /** @@ -1337,17 +1101,18 @@ export const array: ( * import * as O from "./optic.ts"; * import { pipe } from "./fn.ts"; * - * const result = pipe( + * const nums = pipe( * O.id>(), * O.set, - * O.view(new Set([1, 2, 3])), - * ); // [1, 2, 3] + * ); + * + * const result = nums.view(new Set([1, 2, 3])); // [1, 2, 3] * ``` * * @since 2.0.0 */ -export const set: ( - first: Optic>, +export const set: ( + first: Optic, R>, ) => Optic, S, A> = traverse(TraversableSet); /** @@ -1360,17 +1125,18 @@ export const set: ( * import * as T from "./tree.ts"; * import { pipe } from "./fn.ts"; * - * const result = pipe( + * const nums = pipe( * O.id>(), * O.tree, - * O.view(T.tree(1, [T.tree(2, [T.tree(3)])])), - * ); // [1, 2, 3] + * ); + * + * const result = nums.view(T.tree(1, [T.tree(2, [T.tree(3)])])); // [1, 2, 3] * ``` * * @since 2.0.0 */ -export const tree: ( - first: Optic>, +export const tree: ( + first: Optic, R>, ) => Optic, S, A> = traverse(TraversableTree); /** @@ -1387,15 +1153,16 @@ export const tree: ( * * const value = pipe(O.id(), O.prop("value"), O.nil); * - * const result1 = pipe(value, O.view({})); // None - * const result2 = pipe(value, O.view({ value: "Hello" })); // Some("Hello") + * const result1 = value.view({}); // None + * const result2 = value.view({ value: "Hello" }); // Some("Hello") * ``` * * @since 2.0.0 */ -export const nil: ( - first: Optic, -) => Optic, S, NonNullable> = filter(isNotNil); +export const nil: ( + first: Optic, +) => Optic, S, NonNullable, AlignRev> = + filter(isNotNil); /** * A preconstructed composed prism that focuses on the Some value of an Option. @@ -1414,14 +1181,16 @@ export const nil: ( * const brandon: Person = { name: "Brandon", talent: none }; * const emily: Person = { name: "Emily", talent: some("Knitting") }; * - * const result = pipe(talent, O.view([brandon, emily])); // ["Knitting"]; + * const result = talent.view([brandon, emily]); // ["Knitting"]; * ``` * * @since 2.0.0 */ -export const some: ( - optic: Optic>, -) => Optic, S, A> = compose(prism(identity, O.wrap, O.map)); +export const some: ( + optic: Optic, R>, +) => Optic, S, A, AlignRev> = compose( + prism(identity, O.wrap, O.map), +); /** * A preconstructed composed prism that focuses on the Right value of an Either. @@ -1436,17 +1205,17 @@ export const some: ( * * const value = pipe(O.id(), O.right); * - * const result1 = pipe(value, O.view(E.right("Good job!"))); + * const result1 = value.view(E.right("Good job!")); * // Some("Good job!") - * const result2 = pipe(value, O.view(E.left(new Error("Something broke")))); + * const result2 = value.view(E.left(new Error("Something broke"))); * // None * ``` * * @since 2.0.0 */ -export const right: ( - optic: Optic>, -) => Optic, S, A> = compose( +export const right: ( + optic: Optic, R>, +) => Optic, S, A, AlignRev> = compose( prism(E.getRight, E.right, E.map), ); @@ -1463,17 +1232,17 @@ export const right: ( * * const value = pipe(O.id(), O.left); * - * const result1 = pipe(value, O.view(E.right("Good job!"))); + * const result1 = value.view(E.right("Good job!")); * // None - * const result2 = pipe(value, O.view(E.left(new Error("Something broke")))); + * const result2 = value.view(E.left(new Error("Something broke"))); * // Some(Error("Something broke")) * ``` * * @since 2.0.0 */ -export const left: ( - optic: Optic>, -) => Optic, S, B> = compose( +export const left: ( + optic: Optic, R>, +) => Optic, S, B, AlignRev> = compose( prism(E.getLeft, E.left, E.mapSecond), ); @@ -1491,13 +1260,13 @@ export const left: ( * * const numerator = pipe(O.id(), O.first); * - * const result = pipe(numerator, O.view(P.pair(1, 1))); // 1 + * const result = numerator.view(P.pair(1, 1)); // 1 * ``` * * @since 2.0.0 */ -export const first: ( - optic: Optic>, +export const first: ( + optic: Optic, R>, ) => Optic, S, A> = compose(lens(P.getFirst, P.map)); /** @@ -1514,29 +1283,29 @@ export const first: ( * * const denominator = pipe(O.id(), O.first); * - * const result = pipe(denominator, O.view(P.pair(1, 2))); // 2 + * const result = denominator.view(P.pair(1, 2)); // 2 * ``` * * @since 2.0.0 */ -export const second: ( - optic: Optic>, +export const second: ( + optic: Optic, R>, ) => Optic, S, B> = compose(lens(P.getSecond, P.mapSecond)); /** * @since 2.1.0 */ -export const success: ( - optic: Optic>, -) => Optic, S, A> = compose( +export const success: ( + optic: Optic, R>, +) => Optic, S, A, AlignRev> = compose( prism(DE.getSuccess, DE.success, DE.map), ); /** * @since 2.1.0 */ -export const failure: ( - optic: Optic>, -) => Optic, S, B> = compose( +export const failure: ( + optic: Optic, R>, +) => Optic, S, B, AlignRev> = compose( prism(DE.getFailure, DE.failure, DE.mapSecond), ); diff --git a/promise.ts b/promise.ts index 4328dfa..3cf82d7 100644 --- a/promise.ts +++ b/promise.ts @@ -135,7 +135,8 @@ export function abortable( } /** - * Create a Promise that resolve after ms milliseconds. + * Create a Promise that resolve after ms milliseconds that can also be + * disposed early. * * @example * ```ts @@ -153,8 +154,26 @@ export function abortable( * * @since 2.0.0 */ -export function wait(ms: number): Promise { - return new Promise((res) => setTimeout(res, ms)); +export function wait(ms: number): Promise & Disposable { + const disposable = {} as unknown as Disposable; + const result = new Promise((res) => { + let open = true; + const resolve = () => { + if (open) { + open = false; + res(ms); + } + }; + const handle = setTimeout(resolve, ms); + disposable[Symbol.dispose] = () => { + if (open) { + clearTimeout(handle); + resolve(); + } + }; + }); + + return Object.assign(result, disposable); } /** diff --git a/schemable.ts b/schemable.ts index 454e58a..31822d0 100644 --- a/schemable.ts +++ b/schemable.ts @@ -9,7 +9,6 @@ import type { $, Hold, Kind, Spread } from "./kind.ts"; import type { NonEmptyArray } from "./array.ts"; -import type { ReadonlyRecord } from "./record.ts"; import { memoize } from "./fn.ts"; @@ -99,7 +98,7 @@ export interface UndefinableSchemable extends Hold { export interface RecordSchemable extends Hold { readonly record: ( codomain: $, - ) => $, B, C], [D], [E]>; + ) => $, B, C], [D], [E]>; } /** diff --git a/scripts/exports.sh b/scripts/exports.sh index 97b7a03..153be66 100755 --- a/scripts/exports.sh +++ b/scripts/exports.sh @@ -1,3 +1,3 @@ #!/usr/bin/env zsh -for i in *.ts contrib/*.ts; do echo "\"${i:s/\.ts//}\": \"./$i\","; done +for i in *.ts; do echo "\"./${i:s/\.ts//}\": \"./$i\","; done diff --git a/stream.ts b/stream.ts new file mode 100644 index 0000000..2b87a53 --- /dev/null +++ b/stream.ts @@ -0,0 +1,1322 @@ +/** + * The stream module includes construction and combinator functions for a push + * stream datatype. Streams in fun are very close to the Streams of mostjs as + * well as the Observables of rxjs. There are few differences that come from + * a nuanced selection of invariants. Those invariants are: + * + * 1. Streams are lazy by default, and will not start collecting or emitting + * events until they are run. + * 2. A stream must be connected to a sink for events to be consumed. A sink is + * an object with a notion of accepting an event message and an end message. + * 3. When a stream is run by linking it with a sink it will return a + * Disposable, which can be used to cancel the operation of the stream early. + * 4. Once a stream is started it must call end when it completes. If the stream + * is disposed it will not call end. + * + * @module Stream + * @experimental + * @since 2.2.0 + */ +import type { In, Kind, Out } from "./kind.ts"; +import type { Wrappable } from "./wrappable.ts"; +import type { BindTo, Mappable } from "./mappable.ts"; +import type { Applicable } from "./applicable.ts"; +import type { Bind, Flatmappable, Tap } from "./flatmappable.ts"; +import type { Predicate } from "./predicate.ts"; +import type { Refinement } from "./refinement.ts"; +import type { Option } from "./option.ts"; +import type { Either } from "./either.ts"; +import type { Pair } from "./pair.ts"; + +import * as O from "./option.ts"; +import * as E from "./either.ts"; +import * as A from "./array.ts"; +import { createBind, createTap } from "./flatmappable.ts"; +import { createBindTo } from "./mappable.ts"; +import { pair } from "./pair.ts"; +import { flow, pipe } from "./fn.ts"; + +/** + * Represents a sink for receiving values emitted by a stream. + * + * @since 2.2.0 + */ +export type Sink = { + readonly event: (value: A) => void; + readonly end: (reason?: unknown) => void; +}; + +/** + * Represents a stream that emits values of type `A`. + * + * @since 2.2.0 + */ +export type Stream = (sink: Sink, env: R) => Disposable; + +/** + * Specifies Stream as a Higher Kinded Type, with covariant + * parameter A corresponding to the 0th index of any substitutions. + * + * @since 2.2.0 + */ +export interface KindStream extends Kind { + readonly kind: Stream, In>; +} + +/** + * Represents a stream with unknown value type and any environment. + * + * @since 2.2.0 + */ +// deno-lint-ignore no-explicit-any +export type AnyStream = Stream; + +/** + * Extracts the value type from a stream type. + * + * @since 2.2.0 + */ +export type TypeOf = U extends Stream ? A : never; + +/** + * Extracts the environment type from a stream type. + * + * @since 2.2.0 + */ +export type EnvOf = U extends Stream ? R : never; + +/** + * Represents a timeout object with `setTimeout` and `clearTimeout` methods. + * + * @since 2.2.0 + */ +export type Timeout = { + readonly setTimeout: typeof setTimeout; + readonly clearTimeout: typeof clearTimeout; +}; + +/** + * Represents an interval object with `setInterval` and `clearInterval` methods. + */ +export type Interval = { + readonly setInterval: typeof setInterval; + readonly clearInterval: typeof clearInterval; +}; + +/** + * @since 2.2.0 + */ +export type DefaultEnv = Timeout & Interval; + +/** + * Creates a disposable resource with a dispose function. The resource can be disposed of + * by calling the `dispose` method. If `dispose` is called more than once, an error will be thrown. + * + * @param dispose A function that disposes of the resource. + * @returns A Disposable object with a dispose method. + * + * @since 2.2.0 + */ +export function disposable(dispose: () => void): Disposable { + return { [Symbol.dispose]: dispose }; +} + +/** + * Disposes of a disposable resource by calling its dispose method. + * + * @since 2.2.0 + */ +export function dispose(disposable: Disposable): void { + return disposable[Symbol.dispose](); +} + +/** + * Creates a `Disposable` that does nothing when disposed. + * + * @since 2.0.0 + */ +export function disposeNone(): Disposable { + return disposable(() => {}); +} + +/** + * @since 2.2.0 + */ +export function sink( + event: (value: A) => void, + end: (reason?: unknown) => void, +): Sink { + return { event, end }; +} + +/** + * Creates a sink that does nothing when receiving events or ends + * + * @since 2.2.0 + */ +export function emptySink(): Sink { + return sink(NOOP, NOOP); +} + +/** + * Creates a stream with a run function. The run function is responsible for + * managing the interaction with the provided sink and environment. It returns + * a Disposable object that can be used to clean up any resources associated + * with the stream. + * + * @since 2.2.0 + */ +export function stream( + run: (sink: Sink, env: R) => Disposable, +): Stream { + return run; +} + +/** + * Runs a stream until completion, returning a disposable to stop the stream + * early. + * + * @param stream The stream to run. + * @param sink The sink to send event and end messages to. + * @param env The environment to run the stream in. + * @returns A disposable that can cancel the stream. + * + * @since 2.2.0 + */ +export function run(): (stream: Stream) => Disposable; +export function run(env: R): (stream: Stream) => Disposable; +export function run( + env: R, + sink: Sink, +): (stream: Stream) => Disposable; +export function run( + env: R = DefaultEnv as R, + sink: Sink = emptySink(), +): (stream: Stream) => Disposable { + return (stream) => stream(sink, env); +} + +/** + * Runs a stream until completion, returning a promise that resolves when the stream ends. + * + * @param stream The stream to run. + * @param env The environment to run the stream in. + * @returns A promise that resolves when the stream ends. + * + * @since 2.2.0 + */ +export function runPromise(): ( + stream: Stream, +) => Promise; +export function runPromise( + env: R, +): (stream: Stream) => Promise; +export function runPromise( + env: R = DefaultEnv as R, +): (stream: Stream) => Promise { + return (stream) => + new Promise((resolve) => + pipe(stream, run(env, sink(NOOP, resolve))) + ); +} + +/** + * Runs a stream until completion, calling the onEvent when events arrive and + * onEnd when the stream ends. + * + * @param onEvent The function to run on each stream event. + * @param onEnd The function to run on stream end. + * @returns A function that takes a stream and returns a disposable + * + * @since 2.2.0 + */ +export function forEach( + onEvent: (value: A) => void, + onEnd?: (reason?: unknown) => void, +): (ua: Stream) => Disposable; +export function forEach( + onEvent: (value: A) => void, + onEnd: (reason?: unknown) => void, + env: R, +): (ua: Stream) => Disposable; +export function forEach( + onEvent: (value: A) => void = NOOP, + onEnd: (reason?: unknown) => void = NOOP, + env: R = DefaultEnv as R, +): (ua: Stream) => Disposable { + return run(env, sink(onEvent, onEnd)); +} + +/** + * Runs a stream, collecting eny events into an array, then returning the array + * once the stream ends. + * + * @since 2.2.0 + */ +export function collect(): ( + stream: Stream, +) => Promise>; +export function collect( + env: R, +): (stream: Stream) => Promise>; +export function collect( + env: R = DefaultEnv as R, +): (stream: Stream) => Promise> { + return (stream: Stream): Promise> => + new Promise((resolve) => { + const result: A[] = []; + pipe( + stream, + run( + env, + sink((value) => result.push(value), () => resolve(result)), + ), + ); + }); +} + +const NOOP: () => void = () => {}; + +/** + * A Stream instance that emits no values and immediately ends. + */ +const EMPTY: Stream = stream( + (snk) => { + let open = true; + const close = () => open = false; + queueMicrotask(() => open && snk.end()); + return disposable(close); + }, +); + +/** + * Creates an empty `Stream`, which emits no events and ends immediately. + * + * @since 2.2.0 + */ +export function empty(): Stream { + return EMPTY; +} + +/** + * Creates a `Stream` that never emits any events and never ends. + * + * @since 2.2.0 + */ +export function never(): Stream { + return stream(() => disposable(NOOP)); +} + +/** + * Creates a `Stream` that emits a single event when the provided promise resolves, and then ends. + * + * @param ua The promise to convert into a stream. + * @returns A `Stream` that emits the resolved value of the provided promise and then ends. + * + * @since 2.0.0 + */ +export function fromPromise(ua: Promise): Stream { + return stream((snk) => { + let open = true; + const close = () => open = false; + ua.then((value) => { + if (open) { + close(); + snk.event(value); + snk.end(); + } + }); + return disposable(close); + }); +} + +/** + * Creates a `Stream` that emits events from the provided iterable and then ends. + * + * @param values The iterable whose values will be emitted by the stream. + * @returns A `Stream` that emits each value from the provided iterable and then ends. + * + * @since 2.0.0 + */ +export function fromIterable(values: Iterable): Stream { + return stream((snk) => { + let open = true; + const close = () => open = false; + queueMicrotask(() => { + if (open) { + for (const value of values) { + snk.event(value); + } + snk.end(); + } + }); + return disposable(close); + }); +} + +/** + * Creates a stream that emits a single value after a specified delay. + * + * @param time The time in milliseconds after which the value should be emitted. + * @returns A stream that emits a single value after the specified delay. + * + * @since 2.2.0 + */ +export function at(time: number): Stream { + return stream( + (snk: Sink, { setTimeout, clearTimeout }: Timeout) => { + const handle = setTimeout( + () => { + snk.event(time); + snk.end(); + }, + time, + ); + return disposable(() => { + clearTimeout(handle); + }); + }, + ); +} + +/** + * Creates a stream that emits incremental values at regular intervals. + * + * @param period The time interval in milliseconds between each emitted value. + * @returns A stream that emits incremental values at regular intervals. + * + * @since 2.2.0 + */ +export function periodic(period: number): Stream { + return stream((snk: Sink, env: Interval) => { + let start = 0; + const { setInterval, clearInterval } = env; + const handle = setInterval( + () => { + snk.event(start += period); + }, + period, + ); + return disposable(() => { + clearInterval(handle); + }); + }); +} + +/** + * Combines two streams, emitting events from the first stream until it + * ends, and then continuing with events from the second stream. + * + * @param second A function that returns the second stream to be concatenated. + * @returns A function that takes the first stream and returns a new stream concatenating events from both streams. + * + * @since 2.0.0 + */ +export function combine( + second: Stream, +): (first: Stream) => Stream { + return (first: Stream): Stream => + pipe( + fromIterable>([first, second]), + join(1), + ); +} + +/** + * Maps the values of a stream from one type to another using a provided function. + * + * @param fai A function that maps values from type `A` to type `I`. + * @returns A higher-order function that takes a stream of type `A` and returns a new stream of type `I`. + * + * @since 2.2.0 + */ +export function map( + fai: (a: A) => I, +): (ua: Stream) => Stream { + return (ua) => + stream((snk, env) => ua(sink((a) => snk.event(fai(a)), snk.end), env)); +} + +/** + * Wraps a single value into a stream. + * + * @param value The value to be wrapped into the stream. + * @returns A stream containing the provided value. + * + * @since 2.2.0 + */ +export function wrap(value: A): Stream { + return stream((snk) => { + let open = true; + const close = () => open = false; + queueMicrotask(() => { + if (open) { + snk.event(value); + snk.end(); + } + }); + return disposable(close); + }); +} + +/** + * Creates a new stream that only emits values from the original stream that satisfy the provided predicate function. + * + * @param predicate A function that determines whether a value should be emitted (`true`) or filtered out (`false`). + * @returns A higher-order function that takes a stream and returns a new stream containing only the values that satisfy the predicate. + * + * @since 2.2.0 + */ +export function filter( + refinement: (a: A) => a is B, +): (s: Stream) => Stream; +export function filter( + predicate: (a: A) => boolean, +): (s: Stream) => Stream; +export function filter( + predicate: (a: A) => boolean, +): (ua: Stream) => Stream { + return (ua) => + stream((snk, env) => + ua( + sink((value) => { + if (predicate(value)) { + snk.event(value); + } + }, snk.end), + env, + ) + ); +} + +/** + * Apply a filter and mapping operation at the same time against a Stream. + * + * @since 2.2.0 + */ +export function filterMap( + fai: (a: A) => Option, +): (ua: Stream) => Stream { + return (ua) => + stream((snk, env) => + pipe( + ua, + run( + env, + sink((value) => { + const oi = fai(value); + if (O.isSome(oi)) { + snk.event(oi.value); + } + }, snk.end), + ), + ) + ); +} + +/** + * Given a refinement or predicate, return a function that splits an Stream into + * a Pair, Stream>. + * + * @since 2.2.0 + */ +export function partition( + refinement: Refinement, +): (ua: Stream) => Pair, Stream>; +export function partition( + predicate: Predicate, +): (ua: Stream) => Pair, Stream>; +export function partition( + predicate: Predicate, +): (ua: Stream) => Pair, Stream> { + return (ua) => + pair(pipe(ua, filter(predicate)), pipe(ua, filter((a) => !predicate(a)))); +} + +/** + * Map and partition over the inner value of an Stream at the same time. + * + * @since 2.0.0 + */ +export function partitionMap( + fai: (a: A) => Either, +): (ua: Stream) => Pair, Stream> { + return (ua) => + pair( + pipe(ua, filterMap(flow(fai, E.getRight))), + pipe(ua, filterMap(flow(fai, E.getLeft))), + ); +} + +/** + * Creates a new stream by continuously applying a function to a seed value and the values of the original stream. + * + * @param stepper A function that takes the current state and a value from the original stream, and returns an array containing the new state and the value to be emitted by the new stream. + * @param seed The initial state value. + * @returns A higher-order function that takes a stream and returns a new stream resulting from applying the stepper function to each value of the original stream. + * + * @since 2.2.0 + */ +export function loop( + stepper: (state: S, value: A) => [S, B], + seed: S, +): (ua: Stream) => Stream { + return (ua) => + stream((snk, env) => { + let hold: S = seed; + return pipe( + ua, + run( + env, + sink((a) => { + const [seed, value] = stepper(hold, a); + hold = seed; + snk.event(value); + }, snk.end), + ), + ); + }); +} + +/** + * Creates a new stream by accumulating values from the original stream using a provided scanning function. + * + * @param scanner A function that takes the current accumulator value and a value from the original stream, and returns the new accumulator value. + * @param seed The initial accumulator value. + * @returns A higher-order function that takes a stream and returns a new stream with accumulated values. + * + * @since 2.2.0 + */ +export function scan( + scanner: (accumulator: O, value: A) => O, + seed: O, +): (ua: Stream) => Stream { + return (ua) => + stream((snk, env) => { + let state = seed; + return ua( + sink( + (value) => { + state = scanner(state, value); + snk.event(state); + }, + snk.end, + ), + env, + ); + }); +} + +/** + * Creates a new stream by concurrently merging multiple streams into one stream. + * The concurrency of the join can be set and defaults to positive infinity, + * indicating unbounded join. A strategy can also be supplied, which controls + * how inner streams are handled when maximum concurrency is met. + * + * *Hold Strategy*: This strategy keeps an ordered queue of streams to pull from + * when running inner streams end. This strategy has no loss of data. + * + * *Swap Strategy*: This strategy will dispose the oldest running inner stream + * when a new stream arrives to make space for the newest stream. In this + * strategy the oldest streams are where we lose data. + * + * *Drop Strategy": This strategy will ignore any new streams once concurrency + * is maxed. In this strategy newest streams are where we lose data. + * + * There is room for a combination of strategies in the future, but the vast + * majority of behaviors are representable with these three. + * + * @param concurrency The maximum number of inner streams to be running concurrently. + * @param strategy The strategy to use once concurrency is saturated. + * @returns A higher-order function that takes a stream of streams and returns a new stream containing values from all inner streams. + * + * @since 2.2.0 + */ +export function join( + concurrency = Number.POSITIVE_INFINITY, + strategy: "hold" | "swap" | "drop" = "hold", +): ( + ua: Stream, R2>, +) => Stream { + return ( + ua: Stream, R2>, + ): Stream => + stream((outerSnk, env) => { + let outerClosed = false; + const queue: Stream[] = []; + const running = new Map, Disposable>(); // WeakMap? + + /** + * Start inner should only be called when there is enough concurrency to + * support starting a new stream. + */ + function startInner(strm: Stream) { + const innerSnk = sink(outerSnk.event, () => { + /** + * We know that this inner stream has ended so we no longer need to + * dispose of it. Thus we start by removing the sink/disposable pair + * from our running map. + */ + if (running.has(innerSnk)) { + running.delete(innerSnk); + } + + /** + * Next we decide if we should start a new stream by checking the + * concurrency and the queue. There might be a bug here but I haven't + * desk checked it yet. If the call to startInner here leads to a + * sync stream and that stream calls end it might lead to a double + * end call. + * + * I don't think it will because the inner stream started here should + * see running.size === 1, but I'm not entirely sure. + */ + if (running.size < concurrency && queue.length > 0) { + startInner(queue.shift() as Stream); + } + + /** + * Lastly, we check to see if we are the last end call by looking at + * whether the outer stream is closed and whether we are running any + * inner streams still. + */ + if (outerClosed && running.size === 0) { + outerSnk.end(); + } + }); + + /** + * Lastly, we start the innerStream and store it's disposable in + * running, indexed by the innerSnk created above. + */ + running.set(innerSnk, run(env, innerSnk)(strm)); + } + + const dsp = ua( + sink((strm) => { + /** + * If there is enough concurrency to start an inner stream then we + * start it. Otherwise we move on to queueing using the provided + * strategy. + */ + if (running.size < concurrency) { + return startInner(strm); + } + + /** + * For the hold strategy we queue up any inner streams above + * concurrency. As running inner streams end they will be pulled from + * the queue. + */ + if (strategy === "hold") { + return queue.push(strm); + } + + /** + * For the swap strategy we use the unique property of Map that when + * converted to an array it will be put into insertion order. This may + * have some performance impacts, in which case switching to another + * data structure may benefit. For now, the simplicity of this + * implementation wins. + * + * We start by finding the oldest running inner stream, disposing it, + * and starting the new stream. In other libraries this behavior is + * called left switch. + */ + if (strategy === "swap") { + pipe( + Array.from(running), + A.lookup(0), + O.match( + // If there is no running stream then something is likely wrong? + NOOP, + ([oldestSink, oldestDsp]) => { + running.delete(oldestSink); + dispose(oldestDsp); + startInner(strm); + }, + ), + ); + } + + /** + * The drop strategy would go here, however its behavior is + * to ignore new streams so it is a noop. + */ + }, () => { + /** + * There are two cases for end coming from the outer stream. There are + * either inner streams left to process or not. When there are inner + * streams then the startInner function will handle end. If there + * aren't then end must be called here. + */ + outerClosed = true; + + if (running.size === 0 && queue.length == 0) { + outerSnk.end(); + } + }), + env, + ); + + /** + * In join both the outer and inner streams must be disposed. By design, + * disposed streams call end after their cleanup is complete. For join + * the process of dispose is to first dispose of the outer stream if it + * isn't closed, then to clear the queue, and last dispose of the inner + * streams. The last inner stream to be disposed is expected to call end, + * so the machinery of streams should lead to a single call to end. + */ + return disposable(() => { + queue.length = 0; + running.forEach(dispose); + if (!outerClosed) { + dispose(dsp); + } + }); + }); +} + +/** + * Maps each value of the input stream to a new stream using a provided function, + * then flattens the resulting streams into a single stream. + * + * @param faui A function that maps each value of the input stream to a new stream. + * @param count The maximum number of inner streams to be running concurrently. + * @returns A higher-order function that takes a stream and returns a new stream + * containing values from all mapped streams + * + * @since 2.2.0 + */ +export function flatmap( + faui: (a: A) => Stream, + concurrency = Number.POSITIVE_INFINITY, +): (ua: Stream) => Stream { + return (ua) => pipe(ua, map(faui), join(concurrency)); +} + +export function switchmap( + faui: (a: A) => Stream, + concurrency = 1, +): (ua: Stream) => Stream { + return (ua: Stream): Stream => + pipe(ua, map(faui), join(concurrency, "swap")); +} + +export function exhaustmap( + faui: (a: A) => Stream, + concurrency = 1, +): (ua: Stream) => Stream { + return (ua: Stream): Stream => + pipe(ua, map(faui), join(concurrency, "drop")); +} + +/** + * Applies each value of the input stream to a stream of functions, + * producing a stream of results. Apply may lose data if the underlying streams + * push events in a tight loop. This is because in a tight loop the runtime does + * not allow pausing between events from one of the two streams. + * + * @param ua The input stream. + * @returns A higher-order function that takes a stream of functions and returns + * a new stream containing the results of applying each value of the input stream + * to the corresponding function. + * + * @since 2.2.0 + */ +export function apply( + ua: Stream, +): (ufai: Stream<(a: A) => I, R1>) => Stream { + return (ufai: Stream<(a: A) => I, R1>): Stream => + stream((snk, env) => { + let valueDone = false; + let fnDone = false; + let fai: Option<(a: A) => I> = O.none; + let a: Option = O.none; + + function send() { + pipe(fai, O.apply(a), O.tap((i) => snk.event(i))); + } + + const dsp_ufai = ufai( + sink((fn) => { + fai = O.some(fn); + send(); + }, () => { + fnDone = true; + if (valueDone) { + snk.end(); + } + }), + env, + ); + + const dsp_ua = ua( + sink((value) => { + a = O.some(value); + send(); + }, () => { + valueDone = true; + if (fnDone) { + snk.end(); + } + }), + env, + ); + + return disposable(() => { + if (!valueDone) { + dispose(dsp_ua); + } + if (!fnDone) { + dispose(dsp_ufai); + } + }); + }); +} + +/** + * Creates a new stream by combining each value of the input stream with an index value generated by a provided indexing function. + * + * @param indexer A function that takes the current state and returns an array containing the index value and the new state. + * @param seed The initial state value. + * @returns A higher-order function that takes a stream and returns a new stream containing tuples of index-value pairs. + * + * @since 2.2.0 + */ +export function indexed( + indexer: (seed: S) => [I, S], + seed: S, +): (ua: Stream) => Stream<[I, A], R> { + return loop((previous, value) => { + const [index, next] = indexer(previous); + return [next, [index, value]]; + }, seed); +} + +/** + * Creates a new stream by combining each value of the input stream with an index value. + * + * @param start The starting index value. + * @param step The increment step for generating index values. + * @returns A higher-order function that takes a stream and returns a new stream containing tuples of index-value pairs. + * + * @since 2.2.0 + */ +export function withIndex( + start: number = 0, + step: number = 1, +): (ua: Stream) => Stream<[number, A], R> { + return indexed((i) => [i, i + step], start); +} + +/** + * Creates a new stream by combining each value of the input stream with a count index starting from 1. + * + * @param ua The input stream. + * @returns A stream containing tuples of count-value pairs. + * + * @since 2.2.0 + */ +export function withCount( + ua: Stream, +): Stream<[number, A], R> { + return withIndex(1)(ua); +} + +/** + * Creates a new stream by counting the number of values emitted by the input stream. + * + * @param ua The input stream. + * @returns A stream containing the count of values emitted by the input stream. + * + * @since 2.2.0 + */ +export function count(ua: Stream): Stream { + return pipe(ua, withCount, map(([i]) => i)); +} + +/** + * Creates a new stream that emits values from the input stream until a condition specified by the predicate function is met. + * + * @param predicate A function that determines whether to continue emitting values (`true`) or stop emitting values (`false`). + * @returns A higher-order function that takes a stream and returns a new stream containing values until the condition specified by the predicate function is met. + * + * @since 2.2.0 + */ +export function takeUntil( + predicate: (a: A) => boolean, +): (ua: Stream) => Stream { + return (ua) => + stream((snk, env) => { + const dsp = ua( + sink((a) => { + if (predicate(a)) { + snk.event(a); + dispose(dsp); + } else { + snk.event(a); + } + }, snk.end), + env, + ); + return dsp; + }); +} + +/** + * Creates a new stream that emits a specified number of values from the input stream. + * + * @param count The number of values to emit. + * @returns A higher-order function that takes a stream and returns a new stream containing the specified number of values. + * + * @since 2.2.0 + */ +export function take(count: number): (ua: Stream) => Stream { + return (ua) => { + if (count <= 0) { + return empty(); + } + + return stream((snk, env) => { + let index = Math.max(0, count); + const dsp = ua( + sink( + (value) => { + snk.event(value); + if (--index <= 0) { + dispose(dsp); + snk.end(); + } + }, + snk.end, + ), + env, + ); + return dsp; + }); + }; +} + +/** + * Creates a new stream that emits a sequence of numbers starting from a specified value, with a specified step, and up to a specified count. + * + * @param count The number of values to emit. Defaults to positive infinity, emitting an infinite sequence. + * @param start The starting value of the sequence. Defaults to 0. + * @param step The increment step between each value of the sequence. Defaults to 1. + * @returns A stream containing the sequence of numbers. + * + * @since 2.2.0 + */ +export function range( + count: number = Number.POSITIVE_INFINITY, + start = 0, + step = 1, +): Stream { + return stream((snk) => { + let open = true; + const close = () => open = false; + + queueMicrotask(() => { + const length = Math.max(0, Math.floor(count)); + let index = -1; + let value = start; + + while (++index < length) { + if (open) { + snk.event(value); + value += step; + } else { + break; + } + } + snk.end(); + }); + + return disposable(close); + }); +} + +/** + * Creates a new stream by repeating the values emitted by the input stream a specified number of times. + * + * @param count The number of times to repeat the values emitted by the input stream. + * @returns A higher-order function that takes a stream and returns a new stream containing the repeated values. + * + * @since 2.2.0 + */ +export function repeat( + count: number, +): (ua: Stream) => Stream { + return (ua) => + stream((snk, env) => { + let index = Math.floor(Math.max(0, count)); + let dsp = disposeNone(); + let open = true; + const close = () => open = false; + const innerSink = sink(snk.event, () => { + if (open) index-- > 0 ? queueMicrotask(startInner) : snk.end(); + }); + + function startInner() { + if (open) { + dsp = ua(innerSink, env); + } + } + + queueMicrotask(startInner); + + return disposable(() => { + if (open) { + close(); + dispose(dsp); + } + }); + }); +} + +/** + * Shares a parent stream with many sinks. Each parent event is sent to all + * child sinks. When the parent ends, all children sinks receive an end event + * and are cleared from internal structure. If all children sinks are disposed + * the parent is also disposed. + * + * @since 2.2.0 + * @experimental + */ +export function multicast(env: R): (ua: Stream) => Stream { + return (ua: Stream): Stream => { + let dsp = disposeNone(); + const sinks = new Map, Disposable>(); + + return (snk) => { + // Sinks with object equality only get added once. + if (sinks.has(snk)) { + return sinks.get(snk)!; + } + + // The last sink to be disposed disposes the source. + const innerDsp = disposable(() => { + sinks.delete(snk); + if (sinks.size === 0) { + dispose(dsp); + dsp = disposeNone(); + } + }); + + sinks.set(snk, innerDsp); + + // On the first sink addition we start the source + if (sinks.size === 1) { + dsp = ua( + sink( + (value) => sinks.forEach((_, snk) => snk.event(value)), + (reason) => sinks.forEach((_, snk) => snk.end(reason)), + ), + env, + ); + } + + return innerDsp; + }; + }; +} + +/** + * Opens the parent stream only once and multicasts events to all sinks. Only + * the first env is used to start the parent stream. Once all sinks are + * disposed the parent stream is disposed but the last event is still held. + * + * @since 2.2.0 + * @experimental + */ +export function hold(ua: Stream): Stream { + const sinks = new Map, Disposable>(); + let outerDsp: Option = O.none; + let last: Option = O.none; + + return (snk, env) => { + if (sinks.has(snk)) { + return sinks.get(snk)!; + } + + const dsp = disposable(() => { + sinks.delete(snk); + if (sinks.size === 0 && O.isSome(outerDsp)) { + dispose(outerDsp.value); + outerDsp = O.none; + } + }); + + sinks.set(snk, dsp); + + if (O.isNone(outerDsp)) { + outerDsp = O.some(ua( + sink( + (value) => { + last = O.some(value); + sinks.forEach((_, s) => s.event(value)); + }, + (reason) => { + outerDsp = O.none; + sinks.forEach((_, s) => s.end(reason)); + sinks.clear(); + }, + ), + env, + )); + } + + if (O.isSome(last)) { + snk.event(last.value); + } + + return dsp; + }; +} + +export function withLatest( + second: Stream, +): (first: Stream) => Stream<[A1, A2], R1 & R2> { + return (first) => (snk, env) => { + let latest: Option = O.none; + let open = true; + const close = () => open = false; + let secondOpen = true; + const closeSecond = () => secondOpen = false; + + const dspSecond = second( + sink((value) => latest = O.some(value), closeSecond), + env, + ); + + const dspFirst = first( + sink( + (value) => O.isSome(latest) ? snk.event([value, latest.value]) : null, + (reason) => { + close(); + snk.end(reason); + if (secondOpen) { + closeSecond(); + dispose(dspSecond); + } + }, + ), + env, + ); + + return disposable(() => { + if (open) { + close(); + dispose(dspFirst); + } + if (secondOpen) { + closeSecond(); + dispose(dspSecond); + } + }); + }; +} + +export function distinct( + compare: (first: A, second: A) => boolean = (f, s) => f === s, +): (ua: Stream) => Stream { + return (ua) => (snk, env) => { + let last: Option = O.none; + const event = (value: A) => { + last = O.some(value); + snk.event(value); + }; + return ua( + sink( + (value) => { + if (O.isNone(last) || !compare(last.value, value)) { + event(value); + } + }, + snk.end, + ), + env, + ); + }; +} + +/** + * Creates an adapter for creating streams and dispatching values to them. + * + * @returns An array containing a function to dispatch values and a corresponding stream. + * + * @since 2.2.0 + */ +export function createAdapter(): [ + (value: A) => void, + Stream, +] { + const dispatcher = { dispatch: (_: A) => {} }; + const dispatch = (a: A) => dispatcher.dispatch(a); + return [ + dispatch, + pipe( + stream((snk) => { + dispatcher.dispatch = snk.event; + return disposable(() => dispatcher.dispatch = NOOP); + }), + multicast({}), + ), + ]; +} + +/** + * The canonical implementation of Wrappable for Stream. + * + * @since 2.0.0 + */ +export const WrappableStream: Wrappable = { wrap }; + +/** + * The canonical implementation of Mappable for Stream. + * + * @since 2.0.0 + */ +export const MappableStream: Mappable = { map }; + +/** + * The canonical implementation of Applicable for Stream. + * + * @since 2.0.0 + */ +export const ApplicableStream: Applicable = { apply, map, wrap }; + +/** + * The canonical implementation of Flatmappable for Stream. + * + * @since 2.0.0 + */ +export const FlatmappableStream: Flatmappable = { + apply, + flatmap, + map, + wrap, +}; + +/** + * The canonical implementation of Timeout for Stream Environments. + * + * @since 2.0.0 + */ +export const TimeoutEnv: Timeout = { setTimeout, clearTimeout }; + +/** + * The canonical implementation of Interval for Stream Environments. + * + * @since 2.0.0 + */ +export const IntervalEnv: Interval = { setInterval, clearInterval }; + +export const DefaultEnv: Interval & Timeout = { ...TimeoutEnv, ...IntervalEnv }; + +export const tap: Tap = createTap(FlatmappableStream); + +export const bind: Bind = createBind(FlatmappableStream); + +export const bindTo: BindTo = createBindTo(MappableStream); diff --git a/testing/array.test.ts b/testing/array.test.ts index 54a9dca..225a540 100644 --- a/testing/array.test.ts +++ b/testing/array.test.ts @@ -276,6 +276,17 @@ Deno.test("Array binarySearch", () => { assertEquals(search(1000, sorted), 100); }); +Deno.test("Array monoSearch", () => { + const sorted = A.range(100_000); + const search = A.monoSearch(N.SortableNumber); + assertEquals(search(0, sorted), 0); + assertEquals(search(50, sorted), 50); + assertEquals(search(100, sorted), 100); + assertEquals(search(1000, sorted), 1000); + assertEquals(search(100, new Array()), 0); + assertEquals(search(1, [0]), 1); +}); + Deno.test("Array orderedInsert", () => { const even = A.range(5, 0, 2); const ins = A.orderedInsert(N.SortableNumber); diff --git a/testing/async_iterable.test.ts b/testing/async_iterable.test.ts index 6aae2d6..3f09223 100644 --- a/testing/async_iterable.test.ts +++ b/testing/async_iterable.test.ts @@ -25,6 +25,11 @@ Deno.test("AsyncIterable fromIterable", async () => { assertEquals(await AI.collect(asyncIterable), [0, 1, 2]); }); +Deno.test("AsyncIterable fromPromise", async () => { + const asyncIterable = AI.fromPromise(Promise.resolve(1)); + assertEquals(await pipe(AI.collect(asyncIterable)), [1]); +}); + Deno.test("AsyncIterable range", async () => { assertEquals(await pipe(AI.range(3), AI.collect), [0, 1, 2]); }); diff --git a/testing/contrib/fast-check.test.ts b/testing/contrib/fast-check.test.ts deleted file mode 100644 index 9e3313a..0000000 --- a/testing/contrib/fast-check.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; - -import * as FC from "npm:fast-check@3.14.0"; -import * as F from "../../contrib/fast-check.ts"; -import * as R from "../../refinement.ts"; -import { schema } from "../../schemable.ts"; -import { pipe } from "../../fn.ts"; - -Deno.test("Fast Check Schemable", () => { - const Vector = schema((s) => s.tuple(s.number(), s.number(), s.number())); - - const Asteroid = schema((s) => - s.struct({ - type: s.literal("asteroid"), - location: Vector(s), - mass: s.number(), - tags: s.record(s.boolean()), - }) - ); - - const Planet = schema((s) => - s.struct({ - type: s.literal("planet"), - location: Vector(s), - mass: s.number(), - population: s.number(), - habitable: s.boolean(), - }) - ); - - const Rank = schema((s) => - pipe( - s.literal("captain"), - s.union(s.literal("first mate")), - s.union(s.literal("officer")), - s.union(s.literal("ensign")), - ) - ); - - const CrewMember = schema((s) => - pipe( - s.struct({ - name: s.string(), - age: s.number(), - rank: Rank(s), - home: Planet(s), - }), - s.intersect(s.partial({ - tags: s.record(s.string()), - })), - ) - ); - - const Ship = schema((s) => - s.struct({ - type: s.literal("ship"), - location: Vector(s), - mass: s.number(), - name: s.string(), - crew: s.array(CrewMember(s)), - lazy: s.lazy("lazy", () => s.string()), - }) - ); - - const SpaceObject = schema((s) => - pipe(Asteroid(s), s.union(Planet(s)), s.union(Ship(s))) - ); - - const refinement = SpaceObject(R.SchemableRefinement); - const arbitrary = SpaceObject(F.getSchemableArbitrary(FC)); - const rands = FC.sample(arbitrary, 10); - - for (const rand of rands) { - assertEquals(refinement(rand), true); - } -}); diff --git a/testing/contrib/most.test.ts b/testing/contrib/most.test.ts deleted file mode 100644 index 4043cb1..0000000 --- a/testing/contrib/most.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; - -import type { Stream } from "../../contrib/most.ts"; -import * as M from "../../contrib/most.ts"; - -const scheduler = M.newDefaultScheduler(); -const run = (s: Stream): Promise => M.collect(s, scheduler); - -Deno.test("Most wrap", async () => { - assertEquals(await run(M.wrap(1)), [1]); -}); diff --git a/testing/datum_either.test.ts b/testing/datum_either.test.ts new file mode 100644 index 0000000..6899bad --- /dev/null +++ b/testing/datum_either.test.ts @@ -0,0 +1,861 @@ +import { + assertEquals, + assertStrictEquals, +} from "https://deno.land/std@0.103.0/testing/asserts.ts"; + +import * as DE from "../datum_either.ts"; +import * as D from "../datum.ts"; +import * as E from "../either.ts"; +import * as O from "../option.ts"; +import * as N from "../number.ts"; +import * as Sortable from "../sortable.ts"; +import { pipe } from "../fn.ts"; + +Deno.test("DatumEither left", () => { + assertEquals(DE.left(1), D.replete(E.left(1))); +}); + +Deno.test("DatumEither right", () => { + assertEquals(DE.right(1), D.replete(E.right(1))); +}); + +Deno.test("DatumEither initial", () => { + assertEquals(DE.initial, D.initial); +}); + +Deno.test("DatumEither pending", () => { + assertEquals(DE.pending, D.pending); +}); + +Deno.test("DatumEither success", () => { + assertEquals(DE.success(1), D.replete(E.right(1))); + assertEquals(DE.success(1, true), D.refresh(E.right(1))); +}); + +Deno.test("DatumEither failure", () => { + assertEquals(DE.failure(1), D.replete(E.left(1))); + assertEquals(DE.failure(1, true), D.refresh(E.left(1))); +}); + +Deno.test("DatumEither constInitial", () => { + assertStrictEquals(DE.constInitial(), DE.initial); +}); + +Deno.test("DatumEither constPending", () => { + assertStrictEquals(DE.constPending(), DE.pending); +}); + +Deno.test("DatumEither fromNullable", () => { + assertEquals(DE.fromNullable(null), DE.initial); + assertEquals(DE.fromNullable(1), DE.success(1)); +}); + +Deno.test("DatumEither match", () => { + const match = DE.match( + () => 0, + () => 1, + (b: number, refresh: boolean) => refresh ? b + 100 : b, + (a: number, refresh: boolean) => refresh ? a + 100 : a, + ); + + assertEquals(match(DE.initial), 0); + assertEquals(match(DE.pending), 1); + assertEquals(match(DE.failure(2)), 2); + assertEquals(match(DE.failure(2, true)), 102); + assertEquals(match(DE.success(3)), 3); + assertEquals(match(DE.success(3, true)), 103); +}); + +Deno.test("DatumEither tryCatch", () => { + const thrower = (n: number) => { + if (n === 0) { + throw new RangeError("Zero is out of range"); + } + return n; + }; + const catcher = DE.tryCatch(thrower, () => "Error" as const); + assertEquals(catcher(0), DE.failure("Error")); + assertEquals(catcher(1), DE.success(1)); +}); + +Deno.test("DatumEither isSuccess", () => { + assertEquals(DE.isSuccess(DE.initial), false); + assertEquals(DE.isSuccess(DE.pending), false); + assertEquals(DE.isSuccess(DE.success(1)), true); + assertEquals(DE.isSuccess(DE.failure(1)), false); +}); + +Deno.test("DatumEither isFailure", () => { + assertEquals(DE.isFailure(DE.initial), false); + assertEquals(DE.isFailure(DE.pending), false); + assertEquals(DE.isFailure(DE.success(1)), false); + assertEquals(DE.isFailure(DE.failure(1)), true); +}); + +Deno.test("DatumEither fromDatum", () => { + assertEquals(DE.fromDatum(D.initial), DE.initial); + assertEquals(DE.fromDatum(D.pending), DE.pending); + assertEquals(DE.fromDatum(D.replete(1)), DE.success(1)); + assertEquals(DE.fromDatum(D.refresh(1)), DE.success(1, true)); +}); + +Deno.test("DatumEither fromEither", () => { + assertEquals(DE.fromEither(E.right(1)), DE.success(1)); + assertEquals(DE.fromEither(E.left(1)), DE.failure(1)); +}); + +Deno.test("DatumEither getSuccess", () => { + assertEquals(DE.getSuccess(DE.failure(1)), O.none); + assertEquals(DE.getSuccess(DE.success(1)), O.some(1)); +}); + +Deno.test("DatumEither getFailure", () => { + assertEquals(DE.getFailure(DE.failure(1)), O.some(1)); + assertEquals(DE.getFailure(DE.success(1)), O.none); +}); + +Deno.test("DatumEither wrap", () => { + assertEquals(DE.wrap(1), DE.right(1)); +}); + +Deno.test("DatumEither fail", () => { + assertEquals(DE.fail(1), DE.left(1)); +}); + +Deno.test("DatumEither map", () => { + const map = DE.map((n: number) => n + 1); + assertEquals(map(DE.initial), DE.initial); + assertEquals(map(DE.pending), DE.pending); + assertEquals(map(DE.failure(1)), DE.failure(1)); + assertEquals(map(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(map(DE.success(1)), DE.success(2)); + assertEquals(map(DE.success(1, true)), DE.success(2, true)); +}); + +Deno.test("DatumEither mapSecond", () => { + const mapSecond = DE.mapSecond((n: number) => n + 1); + assertEquals(mapSecond(DE.initial), DE.initial); + assertEquals(mapSecond(DE.pending), DE.pending); + assertEquals(mapSecond(DE.failure(1)), DE.failure(2)); + assertEquals(mapSecond(DE.failure(1, true)), DE.failure(2, true)); + assertEquals(mapSecond(DE.success(1)), DE.success(1)); + assertEquals(mapSecond(DE.success(1, true)), DE.success(1, true)); +}); + +Deno.test("DatumEither apply", () => { + // Initial + assertEquals( + pipe( + DE.constInitial<(n: number) => number>(), + DE.apply(DE.initial), + ), + DE.initial, + ); + assertEquals( + pipe( + DE.constInitial<(n: number) => number>(), + DE.apply(DE.pending), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.constInitial<(n: number) => number>(), + DE.apply(DE.failure(1)), + ), + DE.initial, + ); + assertEquals( + pipe( + DE.constInitial<(n: number) => number>(), + DE.apply(DE.failure(1, true)), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.constInitial<(n: number) => number>(), + DE.apply(DE.success(1)), + ), + DE.initial, + ); + assertEquals( + pipe( + DE.constInitial<(n: number) => number>(), + DE.apply(DE.success(1, true)), + ), + DE.pending, + ); + + // Pending + assertEquals( + pipe( + DE.constPending<(n: number) => number>(), + DE.apply(DE.initial), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.constPending<(n: number) => number>(), + DE.apply(DE.pending), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.constPending<(n: number) => number>(), + DE.apply(DE.failure(1)), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.constPending<(n: number) => number>(), + DE.apply(DE.failure(1, true)), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.constPending<(n: number) => number>(), + DE.apply(DE.success(1)), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.constPending<(n: number) => number>(), + DE.apply(DE.success(1, true)), + ), + DE.pending, + ); + + // Failure + assertEquals( + pipe( + DE.failure number>(1), + DE.apply(DE.initial), + ), + DE.initial, + ); + assertEquals( + pipe( + DE.failure number>(1), + DE.apply(DE.pending), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.failure number>(1), + DE.apply(DE.failure(2)), + ), + DE.failure(2), + ); + assertEquals( + pipe( + DE.failure number>(1), + DE.apply(DE.failure(2, true)), + ), + DE.failure(2, true), + ); + assertEquals( + pipe( + DE.failure number>(1), + DE.apply(DE.success(2)), + ), + DE.failure(1), + ); + assertEquals( + pipe( + DE.failure number>(1), + DE.apply(DE.success(2, true)), + ), + DE.failure(1, true), + ); + + // Failure Refreshing + assertEquals( + pipe( + DE.failure number>(1, true), + DE.apply(DE.initial), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.failure number>(1, true), + DE.apply(DE.pending), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.failure number>(1, true), + DE.apply(DE.failure(2)), + ), + DE.failure(2, true), + ); + assertEquals( + pipe( + DE.failure number>(1, true), + DE.apply(DE.failure(2, true)), + ), + DE.failure(2, true), + ); + assertEquals( + pipe( + DE.failure number>(1, true), + DE.apply(DE.success(2, true)), + ), + DE.failure(1, true), + ); + assertEquals( + pipe( + DE.failure number>(1, true), + DE.apply(DE.success(2, true)), + ), + DE.failure(1, true), + ); + + // Success + assertEquals( + pipe( + DE.success((n: number) => n + 100), + DE.apply(DE.initial), + ), + DE.initial, + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100), + DE.apply(DE.pending), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100), + DE.apply(DE.failure(2)), + ), + DE.failure(2), + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100), + DE.apply(DE.failure(2, true)), + ), + DE.failure(2, true), + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100), + DE.apply(DE.success(2)), + ), + DE.success(102), + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100), + DE.apply(DE.success(2, true)), + ), + DE.success(102, true), + ); + + // Success Refreshing + assertEquals( + pipe( + DE.success((n: number) => n + 100, true), + DE.apply(DE.initial), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100, true), + DE.apply(DE.pending), + ), + DE.pending, + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100, true), + DE.apply(DE.failure(2)), + ), + DE.failure(2, true), + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100, true), + DE.apply(DE.failure(2, true)), + ), + DE.failure(2, true), + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100, true), + DE.apply(DE.success(2, true)), + ), + DE.success(102, true), + ); + assertEquals( + pipe( + DE.success((n: number) => n + 100, true), + DE.apply(DE.success(2, true)), + ), + DE.success(102, true), + ); +}); + +Deno.test("DatumEither flatmap", () => { + const flatmapInitial = DE.flatmap((_: number) => DE.initial); + assertEquals(flatmapInitial(DE.initial), DE.initial); + assertEquals(flatmapInitial(DE.pending), DE.pending); + assertEquals(flatmapInitial(DE.failure(1)), DE.failure(1)); + assertEquals(flatmapInitial(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(flatmapInitial(DE.success(1)), DE.initial); + assertEquals(flatmapInitial(DE.success(1, true)), DE.pending); + + const flatmapPending = DE.flatmap((_: number) => DE.pending); + assertEquals(flatmapPending(DE.initial), DE.initial); + assertEquals(flatmapPending(DE.pending), DE.pending); + assertEquals(flatmapPending(DE.failure(1)), DE.failure(1)); + assertEquals(flatmapPending(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(flatmapPending(DE.success(1)), DE.pending); + assertEquals(flatmapPending(DE.success(1, true)), DE.pending); + + const flatmapFailure = DE.flatmap((n: number) => DE.failure(n)); + assertEquals(flatmapFailure(DE.initial), DE.initial); + assertEquals(flatmapFailure(DE.pending), DE.pending); + assertEquals(flatmapFailure(DE.failure(1)), DE.failure(1)); + assertEquals(flatmapFailure(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(flatmapFailure(DE.success(1)), DE.failure(1)); + assertEquals(flatmapFailure(DE.success(1, true)), DE.failure(1, true)); + + const flatmapFailureR = DE.flatmap((n: number) => DE.failure(n, true)); + assertEquals(flatmapFailureR(DE.initial), DE.initial); + assertEquals(flatmapFailureR(DE.pending), DE.pending); + assertEquals(flatmapFailureR(DE.failure(1)), DE.failure(1)); + assertEquals(flatmapFailureR(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(flatmapFailureR(DE.success(1)), DE.failure(1, true)); + assertEquals(flatmapFailureR(DE.success(1, true)), DE.failure(1, true)); + + const flatmapSuccess = DE.flatmap((n: number) => DE.success(n + 1)); + assertEquals(flatmapSuccess(DE.initial), DE.initial); + assertEquals(flatmapSuccess(DE.pending), DE.pending); + assertEquals(flatmapSuccess(DE.failure(1)), DE.failure(1)); + assertEquals(flatmapSuccess(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(flatmapSuccess(DE.success(1)), DE.success(2)); + assertEquals(flatmapSuccess(DE.success(1, true)), DE.success(2, true)); + + const flatmapSuccessR = DE.flatmap((n: number) => DE.success(n + 1, true)); + assertEquals(flatmapSuccessR(DE.initial), DE.initial); + assertEquals(flatmapSuccessR(DE.pending), DE.pending); + assertEquals(flatmapSuccessR(DE.failure(1)), DE.failure(1)); + assertEquals(flatmapSuccessR(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(flatmapSuccessR(DE.success(1)), DE.success(2, true)); + assertEquals(flatmapSuccessR(DE.success(1, true)), DE.success(2, true)); +}); + +Deno.test("DatumEither recover", () => { + const recoverInitial = DE.recover((_: number) => DE.initial); + assertEquals(recoverInitial(DE.initial), DE.initial); + assertEquals(recoverInitial(DE.pending), DE.pending); + assertEquals(recoverInitial(DE.failure(1)), DE.initial); + assertEquals(recoverInitial(DE.failure(1, true)), DE.pending); + assertEquals(recoverInitial(DE.success(1)), DE.success(1)); + assertEquals(recoverInitial(DE.success(1, true)), DE.success(1, true)); + + const recoverPending = DE.recover((_: number) => DE.pending); + assertEquals(recoverPending(DE.initial), DE.initial); + assertEquals(recoverPending(DE.pending), DE.pending); + assertEquals(recoverPending(DE.failure(1)), DE.pending); + assertEquals(recoverPending(DE.failure(1, true)), DE.pending); + assertEquals(recoverPending(DE.success(1)), DE.success(1)); + assertEquals(recoverPending(DE.success(1, true)), DE.success(1, true)); + + const recoverFailure = DE.recover((n: number) => DE.failure(n)); + assertEquals(recoverFailure(DE.initial), DE.initial); + assertEquals(recoverFailure(DE.pending), DE.pending); + assertEquals(recoverFailure(DE.failure(1)), DE.failure(1)); + assertEquals(recoverFailure(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(recoverFailure(DE.success(1)), DE.success(1)); + assertEquals(recoverFailure(DE.success(1, true)), DE.success(1, true)); + + const recoverFailureR = DE.recover((n: number) => DE.failure(n, true)); + assertEquals(recoverFailureR(DE.initial), DE.initial); + assertEquals(recoverFailureR(DE.pending), DE.pending); + assertEquals(recoverFailureR(DE.failure(1)), DE.failure(1, true)); + assertEquals(recoverFailureR(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(recoverFailureR(DE.success(1)), DE.success(1)); + assertEquals(recoverFailureR(DE.success(1, true)), DE.success(1, true)); + + const recoverSuccess = DE.recover((n: number) => DE.success(n + 1)); + assertEquals(recoverSuccess(DE.initial), DE.initial); + assertEquals(recoverSuccess(DE.pending), DE.pending); + assertEquals(recoverSuccess(DE.failure(1)), DE.success(2)); + assertEquals(recoverSuccess(DE.failure(1, true)), DE.success(2, true)); + assertEquals(recoverSuccess(DE.success(1)), DE.success(1)); + assertEquals(recoverSuccess(DE.success(1, true)), DE.success(1, true)); + + const recoverSuccessR = DE.recover((n: number) => DE.success(n + 1, true)); + assertEquals(recoverSuccessR(DE.initial), DE.initial); + assertEquals(recoverSuccessR(DE.pending), DE.pending); + assertEquals(recoverSuccessR(DE.failure(1)), DE.success(2, true)); + assertEquals(recoverSuccessR(DE.failure(1, true)), DE.success(2, true)); + assertEquals(recoverSuccessR(DE.success(1)), DE.success(1)); + assertEquals(recoverSuccessR(DE.success(1, true)), DE.success(1, true)); +}); + +Deno.test("DatumEither alt", () => { + const altInitial = DE.alt(DE.constInitial()); + assertEquals(altInitial(DE.initial), DE.initial); + assertEquals(altInitial(DE.pending), DE.pending); + assertEquals(altInitial(DE.failure(2)), DE.initial); + assertEquals(altInitial(DE.failure(2, true)), DE.initial); + assertEquals(altInitial(DE.success(2)), DE.success(2)); + assertEquals(altInitial(DE.success(2, true)), DE.success(2, true)); + + const altPending = DE.alt(DE.constPending()); + assertEquals(altPending(DE.initial), DE.initial); + assertEquals(altPending(DE.pending), DE.pending); + assertEquals(altPending(DE.failure(2)), DE.pending); + assertEquals(altPending(DE.failure(2, true)), DE.pending); + assertEquals(altPending(DE.success(2)), DE.success(2)); + assertEquals(altPending(DE.success(2, true)), DE.success(2, true)); + + const altFailure = DE.alt(DE.failure(1)); + assertEquals(altFailure(DE.initial), DE.initial); + assertEquals(altFailure(DE.pending), DE.pending); + assertEquals(altFailure(DE.failure(2)), DE.failure(1)); + assertEquals(altFailure(DE.failure(2, true)), DE.failure(1)); + assertEquals(altFailure(DE.success(2)), DE.success(2)); + assertEquals(altFailure(DE.success(2, true)), DE.success(2, true)); + + const altFailureR = DE.alt(DE.failure(1, true)); + assertEquals(altFailureR(DE.initial), DE.initial); + assertEquals(altFailureR(DE.pending), DE.pending); + assertEquals(altFailureR(DE.failure(2)), DE.failure(1, true)); + assertEquals(altFailureR(DE.failure(2, true)), DE.failure(1, true)); + assertEquals(altFailureR(DE.success(2)), DE.success(2)); + assertEquals(altFailureR(DE.success(2, true)), DE.success(2, true)); + + const altSuccess = DE.alt(DE.success(1)); + assertEquals(altSuccess(DE.initial), DE.initial); + assertEquals(altSuccess(DE.pending), DE.pending); + assertEquals(altSuccess(DE.failure(2)), DE.success(1)); + assertEquals(altSuccess(DE.failure(2, true)), DE.success(1)); + assertEquals(altSuccess(DE.success(2)), DE.success(2)); + assertEquals(altSuccess(DE.success(2, true)), DE.success(2, true)); + + const altSuccessR = DE.alt(DE.success(1, true)); + assertEquals(altSuccessR(DE.initial), DE.initial); + assertEquals(altSuccessR(DE.pending), DE.pending); + assertEquals(altSuccessR(DE.failure(2)), DE.success(1, true)); + assertEquals(altSuccessR(DE.failure(2, true)), DE.success(1, true)); + assertEquals(altSuccessR(DE.success(2)), DE.success(2)); + assertEquals(altSuccessR(DE.success(2, true)), DE.success(2, true)); +}); + +Deno.test("DatumEither getShowableDatumEither", () => { + const { show } = DE.getShowableDatumEither( + N.ShowableNumber, + N.ShowableNumber, + ); + assertEquals(show(DE.initial), "Initial"); + assertEquals(show(DE.pending), "Pending"); + assertEquals(show(DE.failure(1)), "Replete(Left(1))"); + assertEquals(show(DE.failure(1, true)), "Refresh(Left(1))"); + assertEquals(show(DE.success(1)), "Replete(Right(1))"); + assertEquals(show(DE.success(1, true)), "Refresh(Right(1))"); +}); + +Deno.test("DatumEither getCombinableDatumEither", () => { + const { combine } = DE.getCombinableDatumEither( + N.CombinableNumberSum, + N.CombinableNumberSum, + ); + + const combineInitial = combine(DE.initial); + assertEquals(combineInitial(DE.initial), DE.initial); + assertEquals(combineInitial(DE.pending), DE.pending); + assertEquals(combineInitial(DE.failure(1)), DE.failure(1)); + assertEquals(combineInitial(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combineInitial(DE.success(1)), DE.success(1)); + assertEquals(combineInitial(DE.success(1, true)), DE.success(1, true)); + + const combinePending = combine(DE.pending); + assertEquals(combinePending(DE.initial), DE.pending); + assertEquals(combinePending(DE.pending), DE.pending); + assertEquals(combinePending(DE.failure(1)), DE.failure(1, true)); + assertEquals(combinePending(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combinePending(DE.success(1)), DE.success(1, true)); + assertEquals(combinePending(DE.success(1, true)), DE.success(1, true)); + + const combineFailure = combine(DE.failure(1)); + assertEquals(combineFailure(DE.initial), DE.failure(1)); + assertEquals(combineFailure(DE.pending), DE.failure(1, true)); + assertEquals(combineFailure(DE.failure(1)), DE.failure(2)); + assertEquals(combineFailure(DE.failure(1, true)), DE.failure(2, true)); + assertEquals(combineFailure(DE.success(1)), DE.failure(1)); + assertEquals(combineFailure(DE.success(1, true)), DE.failure(1, true)); + + const combineFailureR = combine(DE.failure(1, true)); + assertEquals(combineFailureR(DE.initial), DE.failure(1, true)); + assertEquals(combineFailureR(DE.pending), DE.failure(1, true)); + assertEquals(combineFailureR(DE.failure(1)), DE.failure(2, true)); + assertEquals(combineFailureR(DE.failure(1, true)), DE.failure(2, true)); + assertEquals(combineFailureR(DE.success(1)), DE.failure(1, true)); + assertEquals(combineFailureR(DE.success(1, true)), DE.failure(1, true)); + + const combineSuccess = combine(DE.success(1)); + assertEquals(combineSuccess(DE.initial), DE.success(1)); + assertEquals(combineSuccess(DE.pending), DE.success(1, true)); + assertEquals(combineSuccess(DE.failure(1)), DE.failure(1)); + assertEquals(combineSuccess(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combineSuccess(DE.success(1)), DE.success(2)); + assertEquals(combineSuccess(DE.success(1, true)), DE.success(2, true)); + + const combineSuccessR = combine(DE.success(1, true)); + assertEquals(combineSuccessR(DE.initial), DE.success(1, true)); + assertEquals(combineSuccessR(DE.pending), DE.success(1, true)); + assertEquals(combineSuccessR(DE.failure(1)), DE.failure(1, true)); + assertEquals(combineSuccessR(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combineSuccessR(DE.success(1)), DE.success(2, true)); + assertEquals(combineSuccessR(DE.success(1, true)), DE.success(2, true)); +}); + +Deno.test("DatumEither getInitializableDatumEither", () => { + const { init, combine } = DE.getInitializableDatumEither( + N.InitializableNumberSum, + N.InitializableNumberSum, + ); + + assertEquals(init(), DE.initial); + + const combineInitial = combine(DE.initial); + assertEquals(combineInitial(DE.initial), DE.initial); + assertEquals(combineInitial(DE.pending), DE.pending); + assertEquals(combineInitial(DE.failure(1)), DE.failure(1)); + assertEquals(combineInitial(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combineInitial(DE.success(1)), DE.success(1)); + assertEquals(combineInitial(DE.success(1, true)), DE.success(1, true)); + + const combinePending = combine(DE.pending); + assertEquals(combinePending(DE.initial), DE.pending); + assertEquals(combinePending(DE.pending), DE.pending); + assertEquals(combinePending(DE.failure(1)), DE.failure(1, true)); + assertEquals(combinePending(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combinePending(DE.success(1)), DE.success(1, true)); + assertEquals(combinePending(DE.success(1, true)), DE.success(1, true)); + + const combineFailure = combine(DE.failure(1)); + assertEquals(combineFailure(DE.initial), DE.failure(1)); + assertEquals(combineFailure(DE.pending), DE.failure(1, true)); + assertEquals(combineFailure(DE.failure(1)), DE.failure(2)); + assertEquals(combineFailure(DE.failure(1, true)), DE.failure(2, true)); + assertEquals(combineFailure(DE.success(1)), DE.failure(1)); + assertEquals(combineFailure(DE.success(1, true)), DE.failure(1, true)); + + const combineFailureR = combine(DE.failure(1, true)); + assertEquals(combineFailureR(DE.initial), DE.failure(1, true)); + assertEquals(combineFailureR(DE.pending), DE.failure(1, true)); + assertEquals(combineFailureR(DE.failure(1)), DE.failure(2, true)); + assertEquals(combineFailureR(DE.failure(1, true)), DE.failure(2, true)); + assertEquals(combineFailureR(DE.success(1)), DE.failure(1, true)); + assertEquals(combineFailureR(DE.success(1, true)), DE.failure(1, true)); + + const combineSuccess = combine(DE.success(1)); + assertEquals(combineSuccess(DE.initial), DE.success(1)); + assertEquals(combineSuccess(DE.pending), DE.success(1, true)); + assertEquals(combineSuccess(DE.failure(1)), DE.failure(1)); + assertEquals(combineSuccess(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combineSuccess(DE.success(1)), DE.success(2)); + assertEquals(combineSuccess(DE.success(1, true)), DE.success(2, true)); + + const combineSuccessR = combine(DE.success(1, true)); + assertEquals(combineSuccessR(DE.initial), DE.success(1, true)); + assertEquals(combineSuccessR(DE.pending), DE.success(1, true)); + assertEquals(combineSuccessR(DE.failure(1)), DE.failure(1, true)); + assertEquals(combineSuccessR(DE.failure(1, true)), DE.failure(1, true)); + assertEquals(combineSuccessR(DE.success(1)), DE.success(2, true)); + assertEquals(combineSuccessR(DE.success(1, true)), DE.success(2, true)); +}); + +Deno.test("DatumEither getComparableDatumEither", () => { + const { compare } = DE.getComparableDatumEither( + N.ComparableNumber, + N.ComparableNumber, + ); + + const initial = compare(DE.initial); + const pending = compare(DE.pending); + const failure = compare(DE.failure(1)); + const failureR = compare(DE.failure(1, true)); + const success = compare(DE.success(1)); + const successR = compare(DE.success(1, true)); + + assertEquals(initial(DE.initial), true); + assertEquals(initial(DE.pending), false); + assertEquals(initial(DE.failure(1)), false); + assertEquals(initial(DE.failure(1, true)), false); + assertEquals(initial(DE.failure(2)), false); + assertEquals(initial(DE.failure(2, true)), false); + assertEquals(initial(DE.success(1)), false); + assertEquals(initial(DE.success(1, true)), false); + assertEquals(initial(DE.success(2)), false); + assertEquals(initial(DE.success(2, true)), false); + + assertEquals(pending(DE.initial), false); + assertEquals(pending(DE.pending), true); + assertEquals(pending(DE.failure(1)), false); + assertEquals(pending(DE.failure(1, true)), false); + assertEquals(pending(DE.failure(2)), false); + assertEquals(pending(DE.failure(2, true)), false); + assertEquals(pending(DE.success(1)), false); + assertEquals(pending(DE.success(1, true)), false); + assertEquals(pending(DE.success(2)), false); + assertEquals(pending(DE.success(2, true)), false); + + assertEquals(failure(DE.initial), false); + assertEquals(failure(DE.pending), false); + assertEquals(failure(DE.failure(1)), true); + assertEquals(failure(DE.failure(1, true)), false); + assertEquals(failure(DE.failure(2)), false); + assertEquals(failure(DE.failure(2, true)), false); + assertEquals(failure(DE.success(1)), false); + assertEquals(failure(DE.success(1, true)), false); + assertEquals(failure(DE.success(2)), false); + assertEquals(failure(DE.success(2, true)), false); + + assertEquals(failureR(DE.initial), false); + assertEquals(failureR(DE.pending), false); + assertEquals(failureR(DE.failure(1)), false); + assertEquals(failureR(DE.failure(1, true)), true); + assertEquals(failureR(DE.failure(2)), false); + assertEquals(failureR(DE.failure(2, true)), false); + assertEquals(failureR(DE.success(1)), false); + assertEquals(failureR(DE.success(1, true)), false); + assertEquals(failureR(DE.success(2)), false); + assertEquals(failureR(DE.success(2, true)), false); + + assertEquals(success(DE.initial), false); + assertEquals(success(DE.pending), false); + assertEquals(success(DE.failure(1)), false); + assertEquals(success(DE.failure(1, true)), false); + assertEquals(success(DE.failure(2)), false); + assertEquals(success(DE.failure(2, true)), false); + assertEquals(success(DE.success(1)), true); + assertEquals(success(DE.success(1, true)), false); + assertEquals(success(DE.success(2)), false); + assertEquals(success(DE.success(2, true)), false); + + assertEquals(successR(DE.initial), false); + assertEquals(successR(DE.pending), false); + assertEquals(successR(DE.failure(1)), false); + assertEquals(successR(DE.failure(1, true)), false); + assertEquals(successR(DE.failure(2)), false); + assertEquals(successR(DE.failure(2, true)), false); + assertEquals(successR(DE.success(1)), false); + assertEquals(successR(DE.success(1, true)), true); + assertEquals(successR(DE.success(2)), false); + assertEquals(successR(DE.success(2, true)), false); +}); + +Deno.test("DatumEither getSortableDatumEither", () => { + const { sort } = DE.getSortableDatumEither( + N.SortableNumber, + N.SortableNumber, + ); + + assertEquals(sort(DE.initial, DE.initial), 0); + assertEquals(sort(DE.initial, DE.pending), -1); + assertEquals(sort(DE.initial, DE.failure(0)), -1); + assertEquals(sort(DE.initial, DE.failure(0, true)), -1); + assertEquals(sort(DE.initial, DE.failure(1)), -1); + assertEquals(sort(DE.initial, DE.failure(1, true)), -1); + assertEquals(sort(DE.initial, DE.failure(2)), -1); + assertEquals(sort(DE.initial, DE.failure(2, true)), -1); + assertEquals(sort(DE.initial, DE.success(0)), -1); + assertEquals(sort(DE.initial, DE.success(0, true)), -1); + assertEquals(sort(DE.initial, DE.success(1)), -1); + assertEquals(sort(DE.initial, DE.success(1, true)), -1); + assertEquals(sort(DE.initial, DE.success(2)), -1); + assertEquals(sort(DE.initial, DE.success(2, true)), -1); + + assertEquals(sort(DE.pending, DE.initial), 1); + assertEquals(sort(DE.pending, DE.pending), 0); + assertEquals(sort(DE.pending, DE.failure(0)), -1); + assertEquals(sort(DE.pending, DE.failure(0, true)), -1); + assertEquals(sort(DE.pending, DE.failure(1)), -1); + assertEquals(sort(DE.pending, DE.failure(1, true)), -1); + assertEquals(sort(DE.pending, DE.failure(2)), -1); + assertEquals(sort(DE.pending, DE.failure(2, true)), -1); + assertEquals(sort(DE.pending, DE.success(0)), -1); + assertEquals(sort(DE.pending, DE.success(0, true)), -1); + assertEquals(sort(DE.pending, DE.success(1)), -1); + assertEquals(sort(DE.pending, DE.success(1, true)), -1); + assertEquals(sort(DE.pending, DE.success(2)), -1); + assertEquals(sort(DE.pending, DE.success(2, true)), -1); + + assertEquals(sort(DE.failure(1), DE.initial), 1); + assertEquals(sort(DE.failure(1), DE.pending), 1); + assertEquals(sort(DE.failure(1), DE.failure(0)), 1); + assertEquals(sort(DE.failure(1), DE.failure(0, true)), -1); + assertEquals(sort(DE.failure(1), DE.failure(1)), 0); + assertEquals(sort(DE.failure(1), DE.failure(1, true)), -1); + assertEquals(sort(DE.failure(1), DE.failure(2)), -1); + assertEquals(sort(DE.failure(1), DE.failure(2, true)), -1); + assertEquals(sort(DE.failure(1), DE.success(0)), -1); + assertEquals(sort(DE.failure(1), DE.success(0, true)), -1); + assertEquals(sort(DE.failure(1), DE.success(1)), -1); + assertEquals(sort(DE.failure(1), DE.success(1, true)), -1); + assertEquals(sort(DE.failure(1), DE.success(2)), -1); + assertEquals(sort(DE.failure(1), DE.success(2, true)), -1); + + assertEquals(sort(DE.failure(1, true), DE.initial), 1); + assertEquals(sort(DE.failure(1, true), DE.pending), 1); + assertEquals(sort(DE.failure(1, true), DE.failure(0)), 1); + assertEquals(sort(DE.failure(1, true), DE.failure(0, true)), 1); + assertEquals(sort(DE.failure(1, true), DE.failure(1)), 1); + assertEquals(sort(DE.failure(1, true), DE.failure(1, true)), 0); + assertEquals(sort(DE.failure(1, true), DE.failure(2)), 1); + assertEquals(sort(DE.failure(1, true), DE.failure(2, true)), -1); + assertEquals(sort(DE.failure(1, true), DE.success(0)), 1); + assertEquals(sort(DE.failure(1, true), DE.success(0, true)), -1); + assertEquals(sort(DE.failure(1, true), DE.success(1)), 1); + assertEquals(sort(DE.failure(1, true), DE.success(1, true)), -1); + assertEquals(sort(DE.failure(1, true), DE.success(2)), 1); + assertEquals(sort(DE.failure(1, true), DE.success(2, true)), -1); + + assertEquals(sort(DE.success(1), DE.initial), 1); + assertEquals(sort(DE.success(1), DE.pending), 1); + assertEquals(sort(DE.success(1), DE.failure(0)), 1); + assertEquals(sort(DE.success(1), DE.failure(0, true)), -1); + assertEquals(sort(DE.success(1), DE.failure(1)), 1); + assertEquals(sort(DE.success(1), DE.failure(1, true)), -1); + assertEquals(sort(DE.success(1), DE.failure(2)), 1); + assertEquals(sort(DE.success(1), DE.failure(2, true)), -1); + assertEquals(sort(DE.success(1), DE.success(0)), 1); + assertEquals(sort(DE.success(1), DE.success(0, true)), -1); + assertEquals(sort(DE.success(1), DE.success(1)), 0); + assertEquals(sort(DE.success(1), DE.success(1, true)), -1); + assertEquals(sort(DE.success(1), DE.success(2)), -1); + assertEquals(sort(DE.success(1), DE.success(2, true)), -1); + + assertEquals(sort(DE.success(1, true), DE.initial), 1); + assertEquals(sort(DE.success(1, true), DE.pending), 1); + assertEquals(sort(DE.success(1, true), DE.failure(0)), 1); + assertEquals(sort(DE.success(1, true), DE.failure(0, true)), 1); + assertEquals(sort(DE.success(1, true), DE.failure(1)), 1); + assertEquals(sort(DE.success(1, true), DE.failure(1, true)), 1); + assertEquals(sort(DE.success(1, true), DE.failure(2)), 1); + assertEquals(sort(DE.success(1, true), DE.failure(2, true)), 1); + assertEquals(sort(DE.success(1, true), DE.success(0)), 1); + assertEquals(sort(DE.success(1, true), DE.success(0, true)), 1); + assertEquals(sort(DE.success(1, true), DE.success(1)), 1); + assertEquals(sort(DE.success(1, true), DE.success(1, true)), 0); + assertEquals(sort(DE.success(1, true), DE.success(2)), 1); + assertEquals(sort(DE.success(1, true), DE.success(2, true)), -1); +}); diff --git a/testing/contrib/free.test.ts b/testing/free.test.ts similarity index 97% rename from testing/contrib/free.test.ts rename to testing/free.test.ts index 19b3bb2..5b83c0e 100644 --- a/testing/contrib/free.test.ts +++ b/testing/free.test.ts @@ -3,8 +3,8 @@ import { assertStrictEquals, } from "https://deno.land/std@0.103.0/testing/asserts.ts"; -import * as F from "../../contrib/free.ts"; -import { pipe } from "../../fn.ts"; +import * as F from "../free.ts"; +import { pipe } from "../fn.ts"; Deno.test("Free node", () => { assertEquals(F.node(1), { tag: "Node", value: 1 }); diff --git a/testing/optic.test.ts b/testing/optic.test.ts index c278afa..88c3609 100644 --- a/testing/optic.test.ts +++ b/testing/optic.test.ts @@ -41,30 +41,6 @@ Deno.test("Optics _unsafeCast", () => { assertExists(O._unsafeCast(fold, O.FoldTag)); }); -Deno.test("Optics viewer", () => { - const lens = O.viewer(O.LensTag, (n: number) => n); - const affine = O.viewer( - O.AffineTag, - (n: number) => n > 0 ? Op.some(n) : Op.none, - ); - const fold = O.viewer(O.FoldTag, (n: number[]) => n); - - assertEquals(lens.view(1), 1); - assertEquals(affine.view(0), Op.none); - assertEquals(affine.view(1), Op.some(1)); - assertEquals(fold.view([1]), [1]); -}); - -Deno.test("Optics modifier", () => { - const mod = O.modifier(E.map); - assertStrictEquals(mod.modify, E.map); -}); - -Deno.test("Optics reviewer", () => { - const rev = O.reviewer(E.wrap); - assertStrictEquals(rev.review, E.wrap); -}); - Deno.test("Optics optic", () => { const optic = O.optic, number>( O.LensTag, @@ -76,7 +52,7 @@ Deno.test("Optics optic", () => { assertStrictEquals(optic.view, P.getFirst); assertStrictEquals(optic.modify, P.map); - const optic2 = O.optic, number>( + const optic2 = O.optic, number, O.YesRev>( O.AffineTag, identity, Op.map, @@ -203,25 +179,6 @@ Deno.test("Optic fromPredicate", () => { assertStrictEquals(noninit.tag, O.AffineTag); }); -Deno.test("Optic view", () => { - assertEquals(pipe(O.id(), O.view(1)), 1); -}); - -Deno.test("Optic modify", () => { - const mod = pipe(O.id(), O.modify(inc)); - assertEquals(mod(1), 2); -}); - -Deno.test("Optic replace", () => { - const set = pipe(O.id(), O.replace(0)); - assertEquals(set(1), 0); -}); - -Deno.test("Optic review", () => { - const shift = O.iso((n: number) => n + 1, (n) => n - 1); - assertEquals(pipe(shift, O.review(1)), 0); -}); - Deno.test("Optic id", () => { const id = O.id(); @@ -282,21 +239,15 @@ Deno.test("Optic compost", () => { assertStrictEquals(ff.tag, O.FoldTag); }); -Deno.test("Optic composeReviewer", () => { - const trivial = pipe(O.id(), O.composeReviewer(O.id())); - - assertEquals(trivial.review(1), 1); - - const some = () => O.prism, A>(identity, Op.wrap, Op.map); - const arr = () => O.refold, A>(identity, A.wrap, A.map); - const opArr = pipe(some>(), O.composeReviewer(arr())); - - assertEquals(opArr.review(1), Op.some([1])); -}); - Deno.test("Optic wrap", () => { const value = { one: 1 }; - assertStrictEquals(O.wrap(value).view(null), value); + const optic = O.wrap(value); + + assertStrictEquals(optic.view({ one: 2 }), value); + assertEquals(optic.modify(({ one }) => ({ one: one + 1 }))(value), { + one: 2, + }); + assertStrictEquals(optic.review(value), value); }); Deno.test("Optic imap", () => { @@ -335,7 +286,7 @@ Deno.test("Optic prop", () => { Deno.test("Optic index", () => { const atOne = pipe(O.id(), O.index(1)); - const double = pipe(atOne, O.modify((s) => s + s)); + const double = atOne.modify((s) => s + s); const value = ["Hello", "World"]; @@ -348,7 +299,7 @@ Deno.test("Optic index", () => { Deno.test("Optic key", () => { const atOne = pipe(O.id>(), O.key("one")); - const double = pipe(atOne, O.modify((s) => s + s)); + const double = atOne.modify((s) => s + s); const value = { one: "one", two: "two" }; @@ -363,8 +314,8 @@ Deno.test("Optic key", () => { Deno.test("Optic atKey", () => { const atOne = pipe(O.id>(), O.atKey("one")); - const double = pipe(atOne, O.modify(Op.map(inc))); - const remove = pipe(atOne, O.replace(Op.constNone())); + const double = atOne.modify(Op.map(inc)); + const remove = atOne.modify(Op.constNone); const value = { one: 1, two: 2 }; @@ -390,11 +341,8 @@ Deno.test("Optic filter", () => { const filterRefine = pipe(O.id>(), O.filter(refine)); const filterPred = pipe(O.id(), O.filter(pred)); - const refineInc = pipe( - filterRefine, - O.modify(([val]): [number] => [inc(val)]), - ); - const predInc = pipe(filterPred, O.modify(inc)); + const refineInc = filterRefine.modify(([val]): [number] => [inc(val)]); + const predInc = filterPred.modify(inc); assertEquals(filterRefine.view([]), Op.none); assertEquals(filterRefine.view([1]), Op.some([1])); @@ -410,8 +358,8 @@ Deno.test("Optic atMap", () => { const atMap = O.atMap(N.ComparableNumber); const one = pipe(O.id>(), atMap(1)); - const double = pipe(one, O.modify(Op.map(inc))); - const remove = pipe(one, O.replace(Op.constNone())); + const double = one.modify(Op.map(inc)); + const remove = one.modify(Op.constNone); const m1 = new Map(); const m2 = new Map([[1, 1]]); @@ -440,7 +388,7 @@ Deno.test("Optic traverse", () => { O.prop("tree"), O.traverse(T.TraversableTree), ); - const increase = pipe(num, O.modify(inc)); + const increase = num.modify(inc); const s1: State = { tree: T.tree(1) }; const s2: State = { tree: T.tree(1, [T.tree(2, [T.tree(3)])]) }; @@ -475,7 +423,7 @@ Deno.test("Optic combineAll", () => { Deno.test("Optic record", () => { const optic = pipe(O.id>(), O.record); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view({}), []); assertEquals(optic.view({ one: 1, two: 2 }), [1, 2]); @@ -485,7 +433,7 @@ Deno.test("Optic record", () => { Deno.test("Optic array", () => { const optic = pipe(O.id>(), O.array); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view([]), []); assertEquals(optic.view([1, 2]), [1, 2]); @@ -495,7 +443,7 @@ Deno.test("Optic array", () => { Deno.test("Optic set", () => { const optic = pipe(O.id>(), O.set); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(S.init()), []); assertEquals(optic.view(S.set(1, 2)), [1, 2]); @@ -505,7 +453,7 @@ Deno.test("Optic set", () => { Deno.test("Optic tree", () => { const optic = pipe(O.id>(), O.tree); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(T.tree(1)), [1]); assertEquals(optic.view(T.tree(1, [T.tree(2)])), [1, 2]); @@ -515,7 +463,7 @@ Deno.test("Optic tree", () => { Deno.test("Optic nil", () => { const optic = pipe(O.id(), O.nil); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(undefined), Op.none); assertEquals(optic.view(null), Op.none); @@ -527,7 +475,7 @@ Deno.test("Optic nil", () => { Deno.test("Optic some", () => { const optic = pipe(O.id>(), O.some); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(Op.none), Op.none); assertEquals(optic.view(Op.some(1)), Op.some(1)); @@ -537,7 +485,7 @@ Deno.test("Optic some", () => { Deno.test("Optic right", () => { const optic = pipe(O.id>(), O.right); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(E.left(1)), Op.none); assertEquals(optic.view(E.right(1)), Op.some(1)); @@ -547,7 +495,7 @@ Deno.test("Optic right", () => { Deno.test("Optic left", () => { const optic = pipe(O.id>(), O.left); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(E.right(1)), Op.none); assertEquals(optic.view(E.left(1)), Op.some(1)); @@ -557,7 +505,7 @@ Deno.test("Optic left", () => { Deno.test("Optic first", () => { const optic = pipe(O.id>(), O.first); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(P.pair(1, 2)), 1); assertEquals(incr(P.pair(1, 2)), P.pair(2, 2)); @@ -565,7 +513,7 @@ Deno.test("Optic first", () => { Deno.test("Optic second", () => { const optic = pipe(O.id>(), O.second); - const incr = pipe(optic, O.modify(inc)); + const incr = optic.modify(inc); assertEquals(optic.view(P.pair(1, 2)), 2); assertEquals(incr(P.pair(1, 2)), P.pair(1, 3)); diff --git a/testing/promise.test.ts b/testing/promise.test.ts index c2a8de1..dd57df7 100644 --- a/testing/promise.test.ts +++ b/testing/promise.test.ts @@ -71,6 +71,10 @@ Deno.test("Promise wait", async () => { await waiter; const end = Date.now(); assertEquals(end - start >= 100, true); + + { + using _ = P.wait(100); + } }); Deno.test("Promise delay", async () => { diff --git a/testing/stream.test.ts b/testing/stream.test.ts new file mode 100644 index 0000000..7134647 --- /dev/null +++ b/testing/stream.test.ts @@ -0,0 +1,632 @@ +import { assertEquals, assertStrictEquals } from "jsr:@std/assert@0.222.1"; + +import * as S from "../stream.ts"; +import * as P from "../promise.ts"; +import * as O from "../option.ts"; +import * as E from "../either.ts"; +import { pipe } from "../fn.ts"; + +Deno.test("Stream disposable", () => { + let value = 0; + const disposable = S.disposable(() => value++); + S.dispose(disposable); + assertEquals(value, 1); +}); + +Deno.test("Stream dispose", () => { + let value = 0; + const disposable = S.disposable(() => value = 1); + + { + using _ = disposable; + } + + assertEquals(value, 1); +}); + +Deno.test("Stream disposeNone", () => { + { + using _ = S.disposeNone(); + } +}); + +Deno.test("Stream sink", () => { + const events: number[] = []; + const sink = S.sink( + (e: number) => events.push(e), + () => events.push(100), + ); + + sink.event(1); + sink.event(2); + + assertEquals(events, [1, 2]); + + sink.end(); + + assertEquals(events, [1, 2, 100]); +}); + +Deno.test("Stream emptySink", () => { + S.emptySink(); +}); + +Deno.test("Stream empty", async () => { + let ended = false; + let value = 0; + await pipe( + S.empty(), + S.tap((n) => value = n), + S.runPromise({}), + P.then(() => ended = true), + ); + assertEquals(value, 0); + assertEquals(ended, true); +}); + +Deno.test("Stream never", () => { + let value: number = 0; + const disposable = pipe( + S.never(), + S.run({}, S.sink(() => {}, () => value++)), + ); + S.dispose(disposable); + assertEquals(value, 0); +}); + +Deno.test("Stream run", async () => { + let value = 0; + pipe( + S.wrap(1), + S.run( + {}, + S.sink((v) => { + value = v; + }, () => {}), + ), + ); + await P.wait(10); + assertEquals(value, 1); +}); + +Deno.test("Stream runPromise", async () => { + let value = 0; + await pipe( + S.at(1000), + S.tap((v) => value = v), + S.runPromise(), + ); + assertEquals(value, 1000); +}); + +Deno.test("Stream forEach", async () => { + const deferred = P.deferred(); + const values: number[] = []; + let done = false; + pipe( + S.range(3), + S.forEach((n) => values.push(n), () => { + deferred.resolve(null); + done = true; + }), + ); + await deferred; + assertEquals([done, values], [true, [0, 1, 2]]); +}); + +Deno.test("Stream collect", async () => { + const values = await pipe( + S.periodic(100), + S.take(3), + S.collect(S.DefaultEnv), + ); + assertEquals(values, [100, 200, 300]); +}); + +Deno.test("Stream fromPromise", async () => { + const result1 = await pipe( + S.fromPromise(P.wrap(1)), + S.collect({}), + ); + assertEquals(result1, [1]); + + const result2 = await pipe( + S.fromPromise(P.wait(1000)), + S.collect({}), + ); + assertEquals(result2, [1000]); +}); + +Deno.test("Stream fromIterable", async () => { + const result1 = await pipe( + S.fromIterable([1, 2, 3]), + S.collect({}), + ); + assertEquals(result1, [1, 2, 3]); + + const result2 = await pipe( + S.fromIterable(new Set([1, 2, 3])), + S.collect({}), + ); + assertEquals(result2, [1, 2, 3]); + + pipe( + S.fromIterable([1, 2, 3]), + S.run({}), + S.dispose, + ); +}); + +Deno.test("Stream at", async () => { + const result1 = await pipe( + S.at(1000), + S.collect(S.DefaultEnv), + ); + assertEquals(result1, [1000]); +}); + +Deno.test("Stream periodic", async () => { + const result1 = await pipe( + S.periodic(100), + S.take(3), + S.collect(S.DefaultEnv), + ); + assertEquals(result1, [100, 200, 300]); + + const values: number[] = []; + const disposable = pipe( + S.periodic(300), + S.tap((v) => values.push(v)), + S.run(S.DefaultEnv), + ); + await P.wait(1000).then(() => S.dispose(disposable)); + assertEquals(values, [300, 600, 900]); +}); + +Deno.test("Stream combine", async () => { + const result1 = await pipe( + S.wrap(1), + S.combine(S.wrap(2)), + S.collect({}), + ); + assertEquals(result1, [1, 2]); + + const result2 = await pipe( + S.periodic(10), + S.take(3), + S.combine(S.at(10)), + S.take(3), + S.collect(S.DefaultEnv), + ); + assertEquals(result2, [10, 20, 30]); +}); + +Deno.test("Stream filter", async () => { + const result = await pipe( + S.range(10), + S.filter((n) => n % 2 === 0), + S.collect({}), + ); + assertEquals(result, [0, 2, 4, 6, 8]); +}); + +Deno.test("Stream filterMap", async () => { + const result = await pipe( + S.range(10), + S.filterMap(O.fromPredicate((n) => n % 2 === 0)), + S.collect({}), + ); + assertEquals(result, [0, 2, 4, 6, 8]); +}); + +Deno.test("Stream partition", async () => { + const [stream1, stream2] = pipe( + S.range(10), + S.partition((n) => n % 2 === 0), + ); + const result1 = await pipe(stream1, S.collect({})); + assertEquals(result1, [0, 2, 4, 6, 8]); + const result2 = await pipe(stream2, S.collect({})); + assertEquals(result2, [1, 3, 5, 7, 9]); +}); + +Deno.test("Stream partitionMap", async () => { + const [stream1, stream2] = pipe( + S.range(10), + S.partitionMap(E.fromPredicate((n) => n % 2 === 0)), + ); + const result1 = await pipe(stream1, S.collect({})); + assertEquals(result1, [0, 2, 4, 6, 8]); + const result2 = await pipe(stream2, S.collect({})); + assertEquals(result2, [1, 3, 5, 7, 9]); +}); + +Deno.test("Stream loop", async () => { + const result = await pipe( + S.range(3), + S.loop((sum, value) => { + const next = sum + value; + return [next, [next, value]]; + }, 0), + S.collect({}), + ); + assertEquals(result, [[0, 0], [1, 1], [3, 2]]); +}); + +Deno.test("Stream scan", async () => { + const result = await pipe( + S.range(5), + S.scan((a, b) => a + b, 0), + S.collect({}), + ); + assertEquals(result, [0, 1, 3, 6, 10]); +}); + +/** + * @todo Could use many more tests here + */ +Deno.test("Stream join", async () => { + const result1 = await pipe( + S.fromIterable([S.at(100), S.at(200)]), + S.join(), + S.collect(S.DefaultEnv), + ); + assertEquals(result1, [100, 200]); + + const result2 = await pipe( + S.empty(), + S.join(), + S.collect({}), + ); + assertEquals(result2, []); + + const dispose1 = pipe( + S.empty(), + S.join(), + S.run({}), + ); + S.dispose(dispose1); + + const dispose2 = pipe( + S.stream, S.Timeout>( + (snk, { setTimeout, clearTimeout }) => { + let open = true; + snk.event(S.wrap(1)); + const handle = setTimeout(() => { + open = false; + snk.event(S.wrap(2)); + snk.end(); + }, 1000); + return S.disposable(() => { + if (open) { + clearTimeout(handle); + snk.end(); + } + }); + }, + ), + S.join(1), + S.run(S.DefaultEnv), + ); + S.dispose(dispose2); +}); + +Deno.test("Stream flatmap", async () => { + const result = await pipe( + S.periodic(10), + S.take(3), + S.flatmap((n) => pipe(S.at(15), S.map((m) => [n, m]), S.take(3))), + S.collect(S.DefaultEnv), + ); + + assertEquals(result, [[10, 15], [20, 15], [30, 15]]); +}); + +Deno.test("Stream switchmap", async () => { + const result1 = await pipe( + S.periodic(10), + S.take(3), + S.switchmap((n) => pipe(S.periodic(8), S.map((m) => [n, m]), S.take(3))), + S.collect(S.DefaultEnv), + ); + + assertEquals(result1, [[10, 8], [20, 8], [30, 8], [30, 16], [30, 24]]); + + const result2 = await pipe( + S.periodic(10), + S.take(3), + S.switchmap((n) => pipe(S.periodic(8), S.map((m) => [n, m]), S.take(3)), 2), + S.collect(S.DefaultEnv), + ); + + assertEquals(result2, [ + [10, 8], + [10, 16], + [20, 8], + [20, 16], + [30, 8], + [20, 24], + [30, 16], + [30, 24], + ]); +}); + +Deno.test("Stream exhaustmap", async () => { + const result1 = await pipe( + S.periodic(10), + S.take(3), + S.exhaustmap((n) => pipe(S.periodic(8), S.map((m) => [n, m]), S.take(3))), + S.collect(S.DefaultEnv), + ); + + assertEquals(result1, [[10, 8], [10, 16], [10, 24]]); + + const result2 = await pipe( + S.periodic(10), + S.take(3), + S.exhaustmap( + (n) => pipe(S.periodic(8), S.map((m) => [n, m]), S.take(3)), + 2, + ), + S.collect(S.DefaultEnv), + ); + + assertEquals(result2, [[10, 8], [10, 16], [20, 8], [10, 24], [20, 16], [ + 20, + 24, + ]]); +}); + +Deno.test("Stream apply", async () => { + const result1 = await pipe( + S.wrap((n: number) => n + 1), + S.apply(S.wrap(1)), + S.collect({}), + ); + assertEquals(result1, [2]); + + const result2 = await pipe( + S.at(100), + S.map((n) => (m: number) => n + m), + S.apply(S.wrap(1)), + S.collect(S.DefaultEnv), + ); + assertEquals(result2, [101]); + + const disposable1 = pipe( + S.at(100), + S.map((n) => (m: number) => n + m), + S.apply(S.wrap(1)), + S.run(S.DefaultEnv), + ); + S.dispose(disposable1); + + const disposable2 = pipe( + S.wrap((n: number) => n + 1), + S.apply(S.at(1)), + S.run(S.DefaultEnv), + ); + S.dispose(disposable2); +}); + +Deno.test("Stream indexed", async () => { + const result = await pipe( + S.range(3), + S.indexed((n) => [n, n + n], 1), + S.collect({}), + ); + assertEquals(result, [[1, 0], [2, 1], [4, 2]]); +}); + +Deno.test("Stream withIndex", async () => { + const result = await pipe( + S.range(3), + S.withIndex(10, 2), + S.collect({}), + ); + assertEquals(result, [[10, 0], [12, 1], [14, 2]]); +}); + +Deno.test("Stream withCount", async () => { + const result = await pipe( + S.range(3), + S.withCount, + S.collect({}), + ); + assertEquals(result, [[1, 0], [2, 1], [3, 2]]); +}); + +Deno.test("Stream count", async () => { + const result = await pipe( + S.fromIterable(["One", "Two", "Three"]), + S.count, + S.collect({}), + ); + assertEquals(result, [1, 2, 3]); +}); + +Deno.test("Stream takeUntil", async () => { + const result = await pipe( + S.range(100), + S.takeUntil((n) => n >= 2), + S.collect({}), + ); + assertEquals(result, [0, 1, 2]); +}); + +Deno.test("Stream take", async () => { + const result1 = await pipe( + S.range(100), + S.take(0), + S.collect(S.DefaultEnv), + ); + assertEquals(result1, []); + + const result2 = await pipe( + S.range(100), + S.take(3), + S.collect(S.DefaultEnv), + ); + assertEquals(result2, [0, 1, 2]); +}); + +Deno.test("Stream repeat", async () => { + const result1 = await pipe( + S.periodic(10), + S.take(3), + S.repeat(0), + S.collect(S.DefaultEnv), + ); + assertEquals(result1, [10, 20, 30]); + + const result2 = await pipe( + S.periodic(10), + S.take(3), + S.repeat(1), + S.collect(S.DefaultEnv), + ); + assertEquals(result2, [10, 20, 30, 10, 20, 30]); + + let disposed = false; + const dsp = pipe( + S.stream(() => S.disposable(() => disposed = true)), + S.repeat(2), + S.run(), + ); + + await pipe( + P.wait(1), + P.then(() => { + S.dispose(dsp); + assertEquals(disposed, true); + }), + ); +}); + +Deno.test("Stream multicast", async () => { + const stream = pipe( + S.range(10), + S.multicast(S.DefaultEnv), + ); + + const values = new Array(); + pipe( + stream, + S.forEach((value) => values.push(value)), + ); + + const result = await pipe( + stream, + S.collect({}), + ); + + assertEquals(values, result); + + const stream2 = pipe(S.periodic(10), S.multicast(S.DefaultEnv)); + const dsp1 = pipe(stream2, S.run({})); + const dsp2 = pipe(stream2, S.run({})); + S.dispose(dsp1); + S.dispose(dsp2); + + const stream3 = pipe(S.periodic(10), S.multicast(S.DefaultEnv)); + const snk = S.sink(() => {}, () => {}); + const dsp3 = pipe(stream3, S.run({}, snk)); + const dsp4 = pipe(stream3, S.run({}, snk)); + S.dispose(dsp3); + S.dispose(dsp4); +}); + +Deno.test("Stream hold", async () => { + const strm = pipe( + S.at(100), + S.map(() => ({ one: 1 })), + S.hold, + ); + + let first: { one: number }; + pipe( + strm, + S.take(1), + S.forEach((value) => first = value), + ); + + let second: { one: number }; + pipe( + strm, + S.take(1), + S.forEach((value) => second = value), + ); + + await pipe( + P.wait(200), + P.then(() => assertStrictEquals(first, second)), + ); + + let count = 0; + const snk = S.sink(() => count++, () => {}); + const dsp1 = pipe(strm, S.run(S.DefaultEnv, snk)); + const dsp2 = pipe(strm, S.run(S.DefaultEnv, snk)); + assertEquals(count, 1); + S.dispose(dsp1); + S.dispose(dsp2); + + const strm2 = await pipe( + S.wrap(1), + S.hold, + S.collect(S.DefaultEnv), + ); + assertEquals(strm2, [1]); +}); + +Deno.test("Stream withLatest", async () => { + const result = await pipe( + S.periodic(100), + S.take(2), + S.withLatest(S.periodic(60)), + S.collect(S.DefaultEnv), + ); + assertEquals(result, [[100, 60], [200, 180]]); + + const dsp = pipe( + S.periodic(100), + S.withLatest(S.periodic(60)), + S.run(S.DefaultEnv), + ); + S.dispose(dsp); +}); + +Deno.test("Stream distinct", async () => { + const result = await pipe( + S.fromIterable([1, 2, 3, 3, 3, 4]), + S.distinct(), + S.collect(S.DefaultEnv), + ); + + assertEquals(result, [1, 2, 3, 4]); +}); + +Deno.test("Stream createAdapter", async () => { + const [dispatch, strm] = S.createAdapter(); + + const values = new Array(); + pipe( + strm, + S.forEach((value) => values.push(value)), + ); + + dispatch(1); + dispatch(2); + dispatch(3); + + assertEquals(values, [1, 2, 3]); + + const [_, strm2] = S.createAdapter(); + const dsp = pipe(strm2, S.run()); + + await pipe( + P.wait(1), + P.then(() => { + S.dispose(dsp); + }), + ); +});