diff --git a/CHANGELOG.md b/CHANGELOG.md index f28b073..69de501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,36 @@ -# [0.33.0] - 2021-09-02 +# [0.34.0] - 2021-09-04 + +### Added + +- `Reducer` and `Transducer` types. +- `reducer` function to build reducer function. +- `transduce` function to transform foldable instance into some value. +- `map` and `filter` functions that build transducer functions. +- `toArray` and `toList` reducer builders. + +### Changed + +- `take` and `skip` methods of `List` accept a _while_ predicate function now. +- `reduce` method of `List` to accept a `Reducer` only. +- `prepend` method of `List` accepts another `List` only. +- `send` method of `Stream` returns `void`. +- `tap` can accept asynchronous effect. + +### Fixed + +- pass _deep_ parameter to recursive call of `freeze` function. + +### Removed + +- `Compressable` type. +- `compress` method from `List`. +- `uniqueBy` method from `List` because it hides cache implementation. +- `append` method of `List`. +- `StreamEvents` enum. +- `on`, `freeze`, `resume`, `destroy`, `compress` and `uniqueBy` methods from `Stream`. +- `Container`, `Lazy` and `Tuple` monads. + +## [0.33.0] - 2021-09-02 ### Added diff --git a/README.md b/README.md index e6641f5..7d3f27d 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,45 @@ const result3 = memoizedFn(4); // Function is executed memoizedFn.cache.clear(); ``` +### transduce + +```ts +function transduce>( + instance: T, +): ( + aggregator: Reducer, +) => >>( + ...transducers: ChainTransducers +) => I; +``` + +Creates transduce operation over a `Foldable` instance. + +```ts +const result /*: readonly string[] */ = transduce([1, 2, 3])(toArray())( + filter, number>((value) => value >= 2), + map, number, string>(String), +); +``` + +There are two functions that builds transducers: `map` and `filter`. + +### reducer + +```ts +function reducer( + initial: T, +): (fn: (accumulator: T, current: K) => T) => Reducer; +``` + +Helps building reducer. + +```ts +const reduceFunction /*: Reducer */ = reducer(0)(binary('+')); // create reducer that sum numbers. +``` + +There are two predefined reducers that collect value: `toArray` and `toList`. + ### array ```typescript @@ -524,7 +563,7 @@ const getUser /*: (id: string) => User */ = tryCatch( ### tap ```ts -function tap(effect: (value: T) => void): (value: T) => T; +function tap(effect: (value: T) => void | Promise): (value: T) => T; ``` Performs side effect on value while returning it as is. @@ -584,49 +623,6 @@ const result /*: number */ = multiplyIf(9); // Will be returned as is. const result2 /*: number */ = multiplyIf(11); // Will be multiplied. ``` -### wrap - -```typescript -function wrap(value: T): Container; -``` - -Wraps value in `Container` monad and allow perform on it operations in chainable way. - -```typescript -wrap(1) - .map((num) => num + '0') - .chain((str) => wrap(parseInt(str))) - .apply(wrap((num) => Math.pow(num, 2))) - .extract(); // => 100 -``` - -### isContainer - -```typescript -function isContainer(value: unknown): value is Container; -``` - -Check if value is instance of Container. - -```typescript -isContainer(wrap(1)); // true -isContainer(1); // false -``` - -#### Container - -Monad that contains value and allow perform operation on it by set of methods. - -1. `map(fn: (value: T) => R): Container` - maps inner value and returns new `Container` instance with new value. - -2. `chain(fn: (value: T) => Container): Container` - the same as `map`, but function must return already wrapped value. - -3. `apply(other: Container<(value: T) => R>): Container` - maps value by using value of `other` wrapper. Value of other wrapper must be a function type. - -4. `extract(): T` - expose inner value to outside. - -> These methods have also `Option` and `Either` monads. - ### isOption ```typescript @@ -882,43 +878,6 @@ const result /*: boolean */ = isList(list()); Monad that represents lazy `Array`. It can decrease computation step comparably to `Array`. Actual execution of `List`'s methods starts when one of _terminating method_ (method that do not return List instance) is called. -### lazy - -```typescript -function lazy(value: Operation | Lazy): Lazy; -``` - -Creates `Lazy` monad with some operation or from another `Lazy` instance. - -```typescript -const lazyPower /*: Lazy */ = lazy((num: number) => - Math.pow(num, 2), -); -``` - -#### Lazy - -Monad that constructs and compose operations over some value. Similar to `pipe` function, but allows more comprehensive transformation of intermediate values. - -### tuple - -```typescript -function tuple>(...args: T): Tuple; -``` - -Creates `Tuple` from set of elements. - -```typescript -const y /*: Tuple<[number, string]> */ = tuple(9, 'state'); - -// Tuple can be destructured -const [num, str] = y; -``` - -#### Tuple - -Immutable container for fixed sequence of values. - ### stream ```typescript @@ -935,7 +894,7 @@ y.map((value) => Math.pow(value, 2)).listen( ); // Somewhere in the code -y.send(2); // document.body.innerHTML will be equal to 4 +y.send(2); // document.body.innerHTML will set to equal to 4 ``` #### Stream @@ -966,17 +925,10 @@ Monad that allow to defer data initialization. function reviver( key: string, value: JSONValueTypes | SerializabledObject, -): - | JSONValueTypes - | List - | Idle - | Option - | Container - | Either - | Tuple>; +): JSONValueTypes | List | Idle | Option | Either; ``` -Add recognition of `Container`, `Idle`, `Tuple`, `Option`, `List`, `Either` data structures for `JSON.parse`. +Add recognition of `Idle`, `Option`, `List`, `Either` data structures for `JSON.parse`. ```typescript const obj = JSON.parse('{"type":"Some","value":1}', reviver); diff --git a/package.json b/package.json index 3e298c0..d2c6412 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fluss/core", - "version": "0.33.0", + "version": "0.34.0", "description": "Core functions and structures for functional programming.", "keywords": [ "functional-programming", diff --git a/src/array.ts b/src/array.ts index a49f093..3c4431f 100644 --- a/src/array.ts +++ b/src/array.ts @@ -3,15 +3,9 @@ import { isObject } from './is_object'; /** Creates readonly array from set of ArrayLike, Iterable objects or values. */ export const array = ( ...values: ReadonlyArray | Iterable> -): ReadonlyArray => { - // Here must be freezing array operation, - // but due to [this Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=980227) - // it is very slow operation and this action is not performed. - return values - .map((value) => - isObject(value) && ('length' in value || Symbol.iterator in value) - ? Array.from(value as ArrayLike | Iterable) - : Array.of(value as T) - ) - .reduce((accumulator, current) => accumulator.concat(current), []); -}; +): ReadonlyArray => + values.flatMap((value) => + isObject(value) && ('length' in value || Symbol.iterator in value) + ? Array.from(value as ArrayLike | Iterable) + : Array.of(value as T), + ); diff --git a/src/container.ts b/src/container.ts deleted file mode 100644 index 4fcf337..0000000 --- a/src/container.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isObject } from './is_object'; -import { isFunction } from './is_function'; -import type { Typeable, Serializable } from './types'; - -export const CONTAINER_OBJECT_TYPE = '$Container'; - -/** Monad that contains value and allow perform operation on it by set of methods. */ -export interface Container extends Typeable, Serializable { - map(fn: (value: T) => R): Container; - chain(fn: (value: T) => Container): Container; - apply(other: Container<(value: T) => R>): Container; - extract(): T; -} - -/** - * Wraps value in `Container` monad and allow - * to perform on it operations in chainable way. - */ -export const wrap = (value: T): Container => ({ - type: () => CONTAINER_OBJECT_TYPE, - extract: () => value, - toJSON: () => ({ - type: CONTAINER_OBJECT_TYPE, - value, - }), - map: (fn) => wrap(fn(value)), - chain: (fn) => fn(value), - apply: (other) => other.map((fn) => fn(value)), -}); - -/** Check if value is instance of `Container`. */ -export const isContainer = (value: unknown): value is Container => - isObject(value) && - isFunction((value as Typeable).type) && - (value as Typeable).type() === CONTAINER_OBJECT_TYPE; diff --git a/src/either.ts b/src/either.ts index b9962a5..1ff28a3 100644 --- a/src/either.ts +++ b/src/either.ts @@ -1,28 +1,36 @@ import { isObject } from './is_object'; import { isFunction } from './is_function'; -import type { Typeable, Serializable } from './types'; +import type { Typeable, Serializable, Monad, Comonad } from './types'; export const EITHER_LEFT_OBJECT_TYPE = '$Left'; export const EITHER_RIGHT_OBJECT_TYPE = '$Right'; -export interface Right extends Typeable, Serializable { - map(fn: (value: B) => R): Right; - chain>(fn: (value: B) => E): E; - apply(other: Right<(value: B) => R>): Right; - handle(): Right; - isLeft(): this is Right; - isRight(): this is Right; - extract(): B; +export interface Right + extends Typeable, + Monad, + Comonad, + Serializable { + readonly map: (fn: (value: B) => R) => Right; + readonly chain: >(fn: (value: B) => E) => E; + readonly apply: (other: Right<(value: B) => R>) => Right; + readonly handle: () => Right; + readonly isLeft: () => this is Right; + readonly isRight: () => this is Right; + readonly extract: () => B; } -export interface Left extends Typeable, Serializable { - map(): Left; - chain(): Left; - apply(): Left; - isLeft(): this is Left; - isRight(): this is Left; - extract(): A; - handle(fn: (value: A) => B): Right; +export interface Left + extends Typeable, + Monad, + Comonad, + Serializable { + readonly map: () => Left; + readonly chain: () => Left; + readonly apply: () => Left; + readonly isLeft: () => this is Left; + readonly handle: (fn: (value: A) => B) => Right; + readonly isRight: () => this is Left; + readonly extract: () => A; } /** @@ -68,7 +76,7 @@ export const left = (value: A): Left => ({ */ export const either = ( isRight: (value: A | B) => value is B, - value: A | B + value: A | B, ): Either => (isRight(value) ? right(value) : left(value)); /** Checks if value is instance of `Either` monad. */ diff --git a/src/freeze.ts b/src/freeze.ts index 821d193..9f2d5be 100644 --- a/src/freeze.ts +++ b/src/freeze.ts @@ -1,3 +1,5 @@ +import { isObject } from './is_object'; +import { isFunction } from './is_function'; import type { DeepReadonly } from './utilities'; /** @@ -7,13 +9,13 @@ import type { DeepReadonly } from './utilities'; export const freeze = ( value: T, // @ts-ignore - TS cannot assign false to boolean :( - deep: D = false + deep: D = false, ): D extends true ? DeepReadonly : Readonly => { if (deep) { Object.getOwnPropertyNames(value).forEach((name) => { const innerValue = (value as { [key: string]: any })[name]; - if (typeof innerValue === 'object' || typeof innerValue === 'function') { - freeze(innerValue); + if (isObject(innerValue) || isFunction(innerValue)) { + freeze(innerValue, deep); } }); } diff --git a/src/idle.ts b/src/idle.ts index 787bb20..0cdc94a 100644 --- a/src/idle.ts +++ b/src/idle.ts @@ -1,7 +1,7 @@ import { isObject } from './is_object'; import { isNothing } from './is_just_nothing'; import { isFunction } from './is_function'; -import type { Serializable, Typeable } from './types'; +import type { Comonad, Monad, Serializable, Typeable } from './types'; type IdleCallbackHandle = unknown; @@ -20,7 +20,7 @@ declare global { /** Queues a function to be called during a interpreter's idle periods. */ function requestIdleCallback( callback: (deadline: IdleDeadline) => void, - opts?: RequestIdleCallbackOptions + opts?: RequestIdleCallbackOptions, ): IdleCallbackHandle; /** Cancels a callback previously scheduled with `globalThis.requestIdleCallback()`. */ function cancelIdleCallback(handle: IdleCallbackHandle): void; @@ -35,11 +35,15 @@ const cancelCallback = globalThis.cancelIdleCallback ?? globalThis.clearTimeout; export const IDLE_OBJECT_TYPE = '$Idle'; /** Monad that allow to defer data initialization. */ -export interface Idle extends Typeable, Serializable { - map(fn: (value: T) => R): Idle; - chain(fn: (value: T) => Idle): Idle; - apply(other: Idle<(value: T) => R>): Idle; - extract(): T; +export interface Idle + extends Typeable, + Monad, + Comonad, + Serializable { + readonly map: (fn: (value: T) => R) => Idle; + readonly chain: (fn: (value: T) => Idle) => Idle; + readonly apply: (other: Idle<(value: T) => R>) => Idle; + readonly extract: () => T; } /** diff --git a/src/index.ts b/src/index.ts index f6788cc..8b7ecac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,12 +5,10 @@ export * from './fork'; export * from './pipe'; export * from './list'; export * from './when'; -export * from './lazy'; export * from './idle'; export * from './flip'; export * from './curry'; export * from './array'; -export * from './tuple'; export * from './delay'; export * from './types'; export * from './binary'; @@ -26,10 +24,10 @@ export * from './identity'; export * from './throttle'; export * from './is_object'; export * from './utilities'; -export * from './container'; export * from './try_catch'; export * from './consequent'; export * from './is_promise'; +export * from './transducer'; export * from './is_function'; export * from './demethodize'; export * from './concurrently'; diff --git a/src/lazy.ts b/src/lazy.ts deleted file mode 100644 index 4686d13..0000000 --- a/src/lazy.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isObject } from './is_object'; -import { isFunction } from './is_function'; -import type { Typeable } from './types'; - -export const LAZY_OBJECT_TYPE = '$Lazy'; - -/** - * Monad that constructs and compose operations over value. - * Similar to `pipe` function, but allows more comprehensive - * transformation of intermediate values. - */ -export interface Lazy extends Typeable { - /** Perform operation over _value_. */ - run: (value: F) => L; - map(fn: (value: L) => R): Lazy; - chain(fn: (value: L) => Lazy): Lazy; - apply(other: Lazy R>): Lazy; - compose(other: Lazy): Lazy; -} - -export const lazy = (run: (value: F) => L): Lazy => ({ - run, - map: (fn) => lazy((value) => fn(run(value))), - type: () => LAZY_OBJECT_TYPE, - chain: (fn) => lazy((value) => fn(run(value)).run(value)), - apply: (other) => - lazy((value) => other.map((fn) => fn(run(value))).run(value)), - compose: (other) => lazy((value) => other.run(run(value))), -}); - -/** Check if value is `Lazy` instance. */ -export const isLazy = (value: unknown): value is Lazy => - isObject(value) && - isFunction((value as Typeable).type) && - (value as Typeable).type() === LAZY_OBJECT_TYPE; diff --git a/src/list.ts b/src/list.ts index 1aee0da..9638df4 100644 --- a/src/list.ts +++ b/src/list.ts @@ -2,47 +2,56 @@ import { isObject } from './is_object'; import { isFunction } from './is_function'; import { some, Option, none } from './option'; import type { + Monad, Foldable, Typeable, Sizeable, + Semigroup, + Filterable, Serializable, IterableIteratorFunction, } from './types'; export const LIST_OBJECT_TYPE = '$List'; +interface Part { + (count: number): List; + (predicate: (value: T) => boolean): List; +} + /** Monad that represents lazy Array. */ export interface List extends Typeable, Sizeable, + Monad, Iterable, Foldable, + Semigroup, + Filterable, Serializable> { /** * Check if all values of `List` pass _predicate_ function. * If list is empty, then method returns `true`. */ - all(predicate: (value: T) => boolean): boolean; + readonly all: (predicate: (value: T) => boolean) => boolean; /** * Check if at least one value of `List` passes _predicate_ function. * If list is empty, then method returns `false`. */ - any(predicate: (value: T) => boolean): boolean; - has(value: T): boolean; - map(fn: (value: T) => R): List; - sort(fn: (first: T, second: T) => number): List; - take(count: number): List; - skip(count: number): List; - find(predicate: (item: T) => boolean): Option; - chain(fn: (value: T) => List): List; - apply(other: List<(value: T) => R>): List; - filter(predicate: (value: T) => boolean): List; - append(...values: ReadonlyArray): List; - concat(other: List): List; - prepend(...values: ReadonlyArray): List; - asArray(): ReadonlyArray; - uniqueBy(fn: (item: T) => U): List; - forEach(fn: (value: T) => void): void; + readonly any: (predicate: (value: T) => boolean) => boolean; + readonly has: (value: T) => boolean; + readonly map: (fn: (value: T) => R) => List; + readonly sort: (fn: (first: T, second: T) => number) => List; + readonly take: Part; + readonly skip: Part; + readonly find: (predicate: (item: T) => boolean) => Option; + readonly chain: (fn: (value: T) => List) => List; + readonly apply: (other: List<(value: T) => R>) => List; + readonly filter: (predicate: (value: T) => boolean) => List; + readonly concat: (other: List) => List; + readonly prepend: (other: List) => List; + readonly asArray: () => ReadonlyArray; + readonly forEach: (fn: (value: T) => void) => void; } /** Create `List` from function that returns iterable iterator. */ @@ -79,7 +88,9 @@ export const iterate = (over: IterableIteratorFunction): List => ({ } } }), - reduce: (fn, accumulator) => { + reduce: (fn) => { + let accumulator = fn(); + for (const item of over()) { accumulator = fn(accumulator, item); } @@ -110,41 +121,26 @@ export const iterate = (over: IterableIteratorFunction): List => ({ return false; }, - append: (...values) => + prepend: (other) => iterate(function* () { - for (const item of [over(), values]) { + for (const item of [other, over()]) { yield* item; } }), - prepend: (...values) => - iterate(function* () { - for (const item of [values, over()]) { - yield* item; - } - }), - uniqueBy: (fn) => - iterate(function* () { - const unique = new Set>(); - - for (const item of over()) { - const key = fn(item); - - if (!unique.has(key)) { - unique.add(key); - yield item; - } - } - }), sort: (fn) => iterate(function* () { for (const item of Array.from(over()).sort(fn)) { yield item; } }), - take: (count) => + take: (countOrPredicate) => iterate(function* () { + const predicate = isFunction(countOrPredicate) + ? countOrPredicate + : () => 0 <= --(countOrPredicate as number); + for (const item of over()) { - if (0 <= --count) { + if (predicate(item)) { yield item; } else { // Ends iteration even if in parent list are many @@ -154,10 +150,14 @@ export const iterate = (over: IterableIteratorFunction): List => ({ } } }), - skip: (count) => + skip: (countOrPredicate) => iterate(function* () { + const predicate = isFunction(countOrPredicate) + ? countOrPredicate + : () => 0 <= --(countOrPredicate as number); + for (const item of over()) { - if (0 > --count) { + if (!predicate(item)) { yield item; } } diff --git a/src/option.ts b/src/option.ts index cc5d089..104b04c 100644 --- a/src/option.ts +++ b/src/option.ts @@ -2,35 +2,43 @@ import { isJust } from './is_just_nothing'; import { isObject } from './is_object'; import { isFunction } from './is_function'; import type { Just, Nothing } from './utilities'; -import type { Typeable, Serializable } from './types'; +import type { Typeable, Serializable, Monad, Comonad } from './types'; export const OPTION_NONE_OBJECT_TYPE = '$None'; export const OPTION_SOME_OBJECT_TYPE = '$Some'; -export interface Some extends Typeable, Serializable { - map( - fn: (value: T) => R - ): unknown extends R ? Option : R extends Just ? Some : None; - fill(): Some; - chain( - fn: (value: T) => Option - ): R extends Nothing ? None : unknown extends R ? None : Some; - apply( - other: Option<(value: T) => R> - ): R extends Nothing ? None : unknown extends R ? None : Some; - isSome(): this is Some; - isNone(): this is None; - extract(): T; +export interface Some + extends Typeable, + Monad, + Comonad, + Serializable { + readonly map: ( + fn: (value: T) => R, + ) => unknown extends R ? Option : R extends Just ? Some : None; + readonly fill: () => Some; + readonly chain: ( + fn: (value: T) => Option, + ) => R extends Nothing ? None : unknown extends R ? None : Some; + readonly apply: ( + other: Option<(value: T) => R>, + ) => R extends Nothing ? None : unknown extends R ? None : Some; + readonly isSome: () => this is Some; + readonly isNone: () => this is None; + readonly extract: () => T; } -export interface None extends Typeable, Serializable { - map(): None; - fill(fn: () => T): Some; - chain(): None; - apply(): None; - isSome(): this is Some; - isNone(): this is None; - extract(): null; +export interface None + extends Typeable, + Monad, + Comonad, + Serializable { + readonly map: () => None; + readonly fill: (fn: () => T) => Some; + readonly chain: () => None; + readonly apply: () => None; + readonly isSome: () => this is Some; + readonly isNone: () => this is None; + readonly extract: () => null; } /** Monad that encapsulates value that can be undefined at some time. */ @@ -73,7 +81,7 @@ export const some = (value: T): Some => ({ * monad. */ export const maybe = ( - value: T + value: T, ): unknown extends T ? Option : T extends Just ? Some : None => // @ts-ignore isJust(value) ? some(value) : none; diff --git a/src/reviver.ts b/src/reviver.ts index 2baf3a7..032854f 100644 --- a/src/reviver.ts +++ b/src/reviver.ts @@ -1,8 +1,6 @@ import { isObject } from './is_object'; import { List, list, LIST_OBJECT_TYPE } from './list'; import { Idle, idle, IDLE_OBJECT_TYPE } from './idle'; -import { Tuple, tuple, TUPLE_OBJECT_TYPE } from './tuple'; -import { Container, CONTAINER_OBJECT_TYPE, wrap } from './container'; import { maybe, Option, @@ -29,8 +27,6 @@ export type JSONValueTypes = const TYPE_TO_MONAD: Record = { [LIST_OBJECT_TYPE]: list, [IDLE_OBJECT_TYPE]: (value: unknown) => idle(() => value), - [TUPLE_OBJECT_TYPE]: (values: ReadonlyArray) => tuple(...values), - [CONTAINER_OBJECT_TYPE]: wrap, [OPTION_SOME_OBJECT_TYPE]: maybe, [OPTION_NONE_OBJECT_TYPE]: maybe, [EITHER_LEFT_OBJECT_TYPE]: left, @@ -47,14 +43,7 @@ const TYPE_TO_MONAD: Record = { export const reviver = ( _key: string, value: JSONValueTypes | SerializabledObject, -): - | JSONValueTypes - | Idle - | List - | Option - | Container - | Either - | Tuple> => +): JSONValueTypes | Idle | List | Option | Either => isObject(value) && 'type' in value && 'value' in value ? TYPE_TO_MONAD[value['type'] as string]?.(value['value']) ?? value : value; diff --git a/src/stream.ts b/src/stream.ts index 247bbfb..db40fed 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -1,30 +1,22 @@ import { isObject } from './is_object'; import { isFunction } from './is_function'; -import type { Typeable } from './types'; +import type { Filterable, Functor, Semigroup, Typeable } from './types'; export interface StreamListener { (value: T): void; } -export enum StreamEvent { - FREEZE = 'freeze', - RESUME = 'resume', - DESTROY = 'destroy', -} - export const STREAM_OBJECT_TYPE = '$Stream'; /** Structure that makes operations with values over time in live mode. */ -export interface Stream extends Typeable { - /** - * Define listener to specific event of this stream. - * @returns function that detach _listener_ from - * _event_ of this stream. - */ - on(event: StreamEvent, listener: VoidFunction): VoidFunction; +export interface Stream + extends Typeable, + Functor, + Semigroup, + Filterable { map(fn: (value: T) => R): Stream; /** Send _value_ to stream. */ - send(value: T): Stream; + send(value: T): void; concat(other: Stream): Stream; /** * Listen to every value that is passed through stream. @@ -33,159 +25,52 @@ export interface Stream extends Typeable { */ listen(listener: StreamListener): VoidFunction; filter(predicate: (value: T) => boolean): Stream; - /** - * Temporarily forbids stream to accept new values and listeners. - * In contrary of `destroy` method, this method does not remove - * old listeners. - */ - freeze(): Stream; /** * Creates new (**derived**) stream that depends on current stream. * Allow to create custom transform methods that maps over values * of current stream and sends it to derived stream. */ derive(fn: (derived: Stream) => StreamListener): Stream; - /** - * Return to stream ability to accept listeners, values - * and processing them. - */ - resume(): Stream; - /** - * Destroys stream. Stream gets rid of all listeners and - * will be incapable to accept new values and listeners. - * - * Stream can be resumed by `resume` method, but listeners - * need to be attached to stream again. - */ - destroy(): Stream; - uniqueBy(fn: (value: T) => F): Stream; } -interface StreamCreationOptions { - /** - * Flag that tells if Stream has been able to pass - * through itself values. - * - * If value is `false`, then stream cannot accept new values, - * value listeners, destroy listeners and any derived streams - * will never receive values from destroyed stream. - */ - _isActive: boolean; - _valueListeners: Map, StreamListener>; - _freezeListeners: Map; - _resumeListeners: Map; - _destroyListeners: Map; -} +/** Creates live stream. */ +export const stream = (): Stream => { + const _listeners = new Map(); -const createStream = ( - options: StreamCreationOptions = { - _isActive: true, - _valueListeners: new Map(), - _destroyListeners: new Map(), - _freezeListeners: new Map(), - _resumeListeners: new Map(), - }, -): Stream => { const listen = (listener: StreamListener): VoidFunction => { - if (options._isActive) { - options._valueListeners.set(listener, listener); - return () => options._valueListeners.delete(listener); - } else { - return () => {}; - } + _listeners.set(listener, listener); + return () => _listeners.delete(listener); }; const derive = ( fn: (derived: Stream) => StreamListener, ): Stream => { - const derived = createStream(); - derived.on(StreamEvent.DESTROY, listen(fn(derived))); + const derived = stream(); + listen(fn(derived)); return derived; }; return { - on: (event, listener) => { - if (options._isActive) { - switch (event) { - case StreamEvent.FREEZE: - options._freezeListeners.set(listener, listener); - return () => options._freezeListeners.delete(listener); - case StreamEvent.RESUME: - options._resumeListeners.set(listener, listener); - return () => options._resumeListeners.delete(listener); - case StreamEvent.DESTROY: - options._destroyListeners.set(listener, listener); - return () => options._destroyListeners.delete(listener); - default: - return () => {}; - } - } - - return () => {}; - }, map: (fn) => derive((derived) => (value) => derived.send(fn(value))), - send: (value) => { - if (options._isActive) { - options._valueListeners.forEach((fn) => fn(value)); - } - - return createStream(options); - }, + send: (value) => _listeners.forEach((fn) => fn(value)), filter: (predicate) => derive((derived) => (value) => { if (predicate(value)) { derived.send(value); } }), - uniqueBy: (fn) => { - const unique = new Set(); - - return derive((derived) => (value) => { - const key = fn(value); - if (!unique.has(key)) { - unique.add(key); - derived.send(value); - } - }); - }, concat: (other) => { - const concatenated = createStream(); - concatenated.on(StreamEvent.DESTROY, listen(concatenated.send)); - concatenated.on(StreamEvent.DESTROY, other.listen(concatenated.send)); + const concatenated = stream(); + listen(concatenated.send); + other.listen(concatenated.send); return concatenated; }, - freeze: () => { - options._freezeListeners.forEach((fn) => fn()); - options._isActive = false; - - return createStream(options); - }, - resume: () => { - options._isActive = true; - options._resumeListeners.forEach((fn) => fn()); - - return createStream(options); - }, - destroy: () => { - options._destroyListeners.forEach((fn) => fn()); - - options._isActive = false; - options._valueListeners = new Map(); - options._freezeListeners = new Map(); - options._resumeListeners = new Map(); - options._destroyListeners = new Map(); - - return createStream(options); - }, derive, listen, type: () => STREAM_OBJECT_TYPE, }; }; -/** Creates live stream. */ -export const stream = (): Stream => createStream(); - /** Check if _value_ is instance of `Stream` monad. */ export const isStream = (value: unknown): value is Stream => isObject(value) && diff --git a/src/tap.ts b/src/tap.ts index 67a09bb..06a28da 100644 --- a/src/tap.ts +++ b/src/tap.ts @@ -1,6 +1,6 @@ /** Performs side effect on value while returning it as is. */ export const tap = - (effect: (value: T) => void) => + (effect: (value: T) => void | Promise) => (value: T): T => { effect(value); return value; diff --git a/src/task.ts b/src/task.ts index f2ae5b8..75765e0 100644 --- a/src/task.ts +++ b/src/task.ts @@ -1,14 +1,14 @@ import { isObject } from './is_object'; import { isPromise } from './is_promise'; import { isFunction } from './is_function'; -import type { Typeable } from './types'; +import type { Monad, Typeable } from './types'; export type DoneFunction = (value: T) => void; export type FailFunction = (value: E) => void; export type ForkFunction = ( done: DoneFunction, - fail: FailFunction + fail: FailFunction, ) => void; export const TASK_OBJECT_TYPE = '$Task'; @@ -18,16 +18,16 @@ export const TASK_OBJECT_TYPE = '$Task'; * in time (in opposite `Promise` that start doing job immediately * after definition). */ -export interface Task extends Typeable { - start: ForkFunction; - map(fn: (value: T) => R): Task; - chain(fn: (value: T) => Task): Task; - apply(other: Task<(value: T) => R, E>): Task; - asPromise(): Promise; +export interface Task extends Typeable, Monad { + readonly start: ForkFunction; + readonly map: (fn: (value: T) => R) => Task; + readonly chain: (fn: (value: T) => Task) => Task; + readonly apply: (other: Task<(value: T) => R, E>) => Task; + readonly asPromise: () => Promise; } export const task = ( - fork: ForkFunction | Task | Promise + fork: ForkFunction | Task | Promise, ): Task => { const start = isTask(fork) ? fork.start @@ -42,7 +42,7 @@ export const task = ( task((done, fail) => start((value: T) => done(fn(value)), fail)), chain: (fn) => task((done, fail) => - start((value: T) => fn(value).start(done, fail), fail) + start((value: T) => fn(value).start(done, fail), fail), ), apply: (other) => task((done, fail) => { @@ -60,7 +60,7 @@ export const fail = (value: E): Task => /** Check if _value_ is instance of `Task` monad. */ export const isTask = ( - value: unknown + value: unknown, ): value is Task => isObject(value) && isFunction((value as Typeable).type) && diff --git a/src/transducer.ts b/src/transducer.ts new file mode 100644 index 0000000..8a5a726 --- /dev/null +++ b/src/transducer.ts @@ -0,0 +1,67 @@ +import { pipe } from './pipe'; +import { list } from './list'; +import { NArray } from './utilities'; +import { isNothing } from './is_just_nothing'; +import { Foldable, Reducer, Transducer } from './types'; + +type ChainTransducers< + O extends ReadonlyArray>, + T extends ReadonlyArray> = [], +> = NArray.Length extends number + ? O + : NArray.Length extends 0 + ? T + : ChainTransducers< + NArray.Tail, + [ + ...T, + ReturnType> extends NArray.First< + Parameters> + > + ? NArray.First + : never, + ] + >; + +/** Creates a reducer. */ +export const reducer = + (initial: T) => + (fn: (accumulator: T, current: K) => T): Reducer => + (accumulator: T = initial, current?: K): T => + isNothing(current) ? accumulator : fn(accumulator, current); + +/** Creates transduce operation over a `Foldable` instance. */ +export const transduce = + >(instance: T) => + (aggregator: Reducer) => + >>( + ...transducers: ChainTransducers + ): I => + // @ts-ignore + instance.reduce(pipe(...transducers)(aggregator), aggregator()); + +/** Creates a _transformer_ transducer. */ +export const map = + (fn: (value: R) => K): Transducer => + (inner: Reducer) => + reducer(inner())((accumulator, current) => inner(accumulator, fn(current))); + +/** Creates a _filter_ transducer. */ +export const filter = + (predicate: (value: R) => boolean): Transducer => + (inner: Reducer) => + reducer(inner())((accumulator, current) => + predicate(current) ? inner(accumulator, current) : accumulator, + ); + +/** Collect result of transducing into an array. */ +export const toArray = () => + reducer>([])((accumulator, current: T) => + accumulator.concat([current]), + ); + +/** Collect result of transducing into an `List`. */ +export const toList = () => + reducer(list())((accumulator, current: T) => + accumulator.concat(list(current)), + ); diff --git a/src/tuple.ts b/src/tuple.ts deleted file mode 100644 index a5b0655..0000000 --- a/src/tuple.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { NArray } from './utilities'; -import { isObject } from './is_object'; -import { isFunction } from './is_function'; -import { Typeable, Sizeable, Serializable } from './types'; - -export const TUPLE_OBJECT_TYPE = '$Tuple'; - -/** Immutable container for fixed sequence of values. */ -export interface Tuple> - extends Iterable, - Typeable, - Sizeable, - Serializable { - pop(count?: C): Tuple>; - item

(position: P): T[P]; - shift(count?: C): Tuple>; - append>(...values: V): Tuple<[...T, ...V]>; - concat>( - other: Tuple - ): Tuple<[...T, ...V]>; - prepend>(...values: V): Tuple<[...V, ...T]>; - position(value: V): number; - transform

( - position: P, - fn: (value: T[P]) => R - ): Tuple>; - asArray(): T; -} - -/** Creates `Tuple` based on given values. */ -export const tuple = >( - ...values: T -): Tuple => ({ - type: () => TUPLE_OBJECT_TYPE, - [Symbol.iterator]: function* () { - for (const item of values) { - yield item; - } - }, - toJSON: () => ({ - type: TUPLE_OBJECT_TYPE, - value: values, - }), - size: () => values.length, - item: (position) => values[position], - transform:

(position: P, fn: (value: T[P]) => R) => - tuple( - ...(values.map((value, index) => - index === position ? fn(value) : value - ) as NArray.Transform) - ), - position: (value) => values.indexOf(value), - append: >(...items: V) => - tuple(...(values.concat(items) as [...T, ...V])), - concat: >(other: Tuple) => - tuple<[...T, ...V]>(...(values.concat([...other]) as [...T, ...V])), - prepend: >(...items: V) => - tuple(...(items.concat(values) as [...V, ...T])), - shift: (count = 1) => - tuple>(...(values.slice(count) as NArray.Shift)), - pop: (count = 1) => - tuple>( - ...(values.slice(0, values.length - count) as NArray.Pop) - ), - isEmpty: () => values.length === 0, - asArray: () => values, -}); - -/** Check if value is instance of `Tuple`. */ -export const isTuple = >( - value: unknown -): value is Tuple => - isObject(value) && - isFunction((value as Typeable).type) && - (value as Typeable).type() === TUPLE_OBJECT_TYPE; diff --git a/src/types.ts b/src/types.ts index 8be7017..7737235 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,9 +23,19 @@ export interface Comonad extends Functor { export interface Monad extends Chain {} +export interface Reducer { + (accumulator: I, current: V): I; + // It is needed for proper TypeScript inference. + (accumulator?: I, current?: V): I; +} + +export interface Transducer { + (reducer: Reducer): Reducer; +} + export interface Foldable { /** Reduce iterable to some value. */ - reduce(fn: (accumulator: R, value: T) => R, accumulator: R): R; + reduce(fn: Reducer): R; } export interface Filterable { diff --git a/test/container.spec.ts b/test/container.spec.ts deleted file mode 100644 index d43fb66..0000000 --- a/test/container.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { wrap, isContainer, CONTAINER_OBJECT_TYPE } from '../src/container'; - -describe('Container', () => { - test('wrap function wraps value into Container', () => { - expect(typeof wrap(4)).toBe('object'); - }); - - test('wrap function wraps value into Container and extract it', () => { - expect(wrap(4).extract()).toBe(4); - }); - - test('wrap function wraps value into Container and can map it', () => { - expect( - wrap(4) - .map((u) => String(u)) - .extract() - ).toBe('4'); - - expect( - wrap(4) - .chain((u) => wrap(String(u))) - .extract() - ).toBe('4'); - - expect( - wrap(4) - .apply(wrap((u) => String(u))) - .extract() - ).toBe('4'); - }); - - test('isContainer checks if value is Container instance', () => { - expect(isContainer(wrap(8))).toBe(true); - expect(isContainer('')).toBe(false); - }); - - test('should be serializable', () => { - expect(wrap(5).toJSON()).toEqual({ type: CONTAINER_OBJECT_TYPE, value: 5 }); - }); -}); diff --git a/test/lazy.spec.ts b/test/lazy.spec.ts deleted file mode 100644 index 1ce9500..0000000 --- a/test/lazy.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { lazy, isLazy } from '../src/lazy'; - -describe('lazy', () => { - const container = lazy((number: number) => number); - - test('should creates wrapper over function', () => { - expect(isLazy(container)).toBe(true); - }); - - test('should map value', () => { - const mapped = container.map((number) => Math.pow(number, 2)); - - expect(mapped.run(2)).toBe(4); - }); - - test('should map value with chain method', () => { - const chained = container.chain((number) => - lazy((first) => first + number + '!') - ); - - expect(chained.run(1)).toBe('2!'); - }); - - test('should perform application operation', () => { - const applied = container.apply( - lazy((number) => (result) => number + result) - ); - - expect(applied.run(1)).toBe(2); - }); -}); diff --git a/test/list.spec.ts b/test/list.spec.ts index 3a7e030..17f5063 100644 --- a/test/list.spec.ts +++ b/test/list.spec.ts @@ -1,4 +1,12 @@ -import { list, iterate, isList, isOption, LIST_OBJECT_TYPE } from '../src'; +import { + list, + isList, + binary, + iterate, + reducer, + isOption, + LIST_OBJECT_TYPE, +} from '../src'; describe('List data structure', () => { test('list and iterate functions create List container', () => { @@ -52,12 +60,8 @@ describe('List data structure', () => { ).toEqual([4, 5, 6]); }); - test('append method add values to list', () => { - expect(list(4).append(1, 2, 3).asArray()).toEqual([4, 1, 2, 3]); - }); - test('prepend method add values to list', () => { - expect(list(4).prepend(1, 2, 3).asArray()).toEqual([1, 2, 3, 4]); + expect(list(4).prepend(list(1, 2, 3)).asArray()).toEqual([1, 2, 3, 4]); }); test( @@ -87,11 +91,11 @@ describe('List data structure', () => { }); test('reduce method reduce values of List to one value', () => { - expect(list(1, 2, 3).reduce((a, v) => a + v, 0)).toBe(6); + expect(list(1, 2, 3).reduce(reducer(0)(binary('+')))).toBe(6); }); test('reduce method must return accumulator value if list is empty', () => { - expect(list().reduce((a, v) => a + v, 0)).toBe(0); + expect(list().reduce(reducer(0)(binary('+')))).toBe(0); }); test('any method check if at least one value pass predicate', () => { @@ -127,28 +131,6 @@ describe('List data structure', () => { expect(list(1, 2, 3, 4, 5).take(3).asArray()).toEqual([1, 2, 3]); }); - test('uniqueBy skips duplicate values by id property', () => { - expect( - list({ id: 1 }, { id: 2 }, { id: 2 }, { id: 2 }, { id: 5 }) - .uniqueBy((item) => item.id) - .asArray(), - ).toEqual([{ id: 1 }, { id: 2 }, { id: 5 }]); - }); - - test('uniqueBy skips values by id property of inner object', () => { - expect( - list( - { o: { id: 1 } }, - { o: { id: 2 } }, - { o: { id: 2 } }, - { o: { id: 2 } }, - { o: { id: 5 } }, - ) - .uniqueBy((item) => item.o.id) - .asArray(), - ).toEqual([{ o: { id: 1 } }, { o: { id: 2 } }, { o: { id: 5 } }]); - }); - test('skip method should skip 3 values', () => { expect(list(1, 3, 4, 5, 6).skip(3).asArray()).toEqual([5, 6]); }); @@ -167,4 +149,18 @@ describe('List data structure', () => { expect(serializabledObject).toMatch(`"type":"${LIST_OBJECT_TYPE}"`); expect(serializabledObject).toMatch('"value":[1,2,3]'); }); + + it('should take values until predicate is truthy', () => { + const a = list(1, 2, 3, 4, 5, 6, 7, 8, 9); + + const result = a.take((item) => item < 6); + expect(result.asArray()).toEqual([1, 2, 3, 4, 5]); + }); + + it('should skip values until it reaches `true` for a given predicate', () => { + const a = list(1, 2, 3, 4, 5, 6, 7, 8, 9); + + const result = a.skip((item) => item > 6); + expect(result.asArray()).toEqual([1, 2, 3, 4, 5, 6]); + }); }); diff --git a/test/pipe.spec.ts b/test/pipe.spec.ts index bfcde75..37189bd 100644 --- a/test/pipe.spec.ts +++ b/test/pipe.spec.ts @@ -4,7 +4,7 @@ describe('pipe', () => { test('should compose two functions and return function.', () => { const composedFn = pipe( (n: number) => `${n} is number`, - (n) => n + '!' + (n) => n + '!', ); expect(composedFn(9)).toBe('9 is number!'); @@ -18,13 +18,13 @@ describe('pipe', () => { const composedFn: any = pipe(); expect(composedFn(6, 5, 4)).toEqual([6, 5, 4]); - } + }, ); test('should compose asynchronous functions.', async () => { const composed = pipe( async (s: string) => s, - async (s) => parseInt(s) + async (s: string) => parseInt(s), ); expect(await composed('1')).toBe(1); @@ -39,7 +39,7 @@ describe('pipe', () => { it('should compose functions with one mandatory parameter and with variadic parameter', () => { const composed = pipe( (n: number) => n, - (...args: Array) => args.reduce((a, c) => a + c, 0) + (...args: Array) => args.reduce((a, c) => a + c, 0), ); expect(composed(1)).toBe(1); diff --git a/test/reviver.spec.ts b/test/reviver.spec.ts index 5b128fd..982dd4c 100644 --- a/test/reviver.spec.ts +++ b/test/reviver.spec.ts @@ -1,15 +1,11 @@ import { isList, isIdle, - isTuple, reviver, isEither, isOption, - isContainer, LIST_OBJECT_TYPE, IDLE_OBJECT_TYPE, - TUPLE_OBJECT_TYPE, - CONTAINER_OBJECT_TYPE, OPTION_SOME_OBJECT_TYPE, EITHER_LEFT_OBJECT_TYPE, EITHER_RIGHT_OBJECT_TYPE, @@ -43,16 +39,6 @@ describe('reviver', () => { expect(JSON.parse(json, reviver).isLeft()).toBe(true); }); - test('should create Container instance', () => { - const json = `{"type":"${CONTAINER_OBJECT_TYPE}","value":5}`; - expect(isContainer(JSON.parse(json, reviver))).toBe(true); - }); - - test('should create Tuple instance', () => { - const json = `{"type":"${TUPLE_OBJECT_TYPE}","value":[5]}`; - expect(isTuple(JSON.parse(json, reviver))).toBe(true); - }); - test('should create an Idle instance', () => { const json = `{"type":"${IDLE_OBJECT_TYPE}","value":7}`; expect(isIdle(JSON.parse(json, reviver))).toBe(true); diff --git a/test/stream.spec.ts b/test/stream.spec.ts index f950d30..d67612e 100644 --- a/test/stream.spec.ts +++ b/test/stream.spec.ts @@ -1,4 +1,4 @@ -import { stream, isStream, StreamEvent } from '../src'; +import { stream, isStream } from '../src'; describe('stream', () => { test('should creates stream object', () => { @@ -11,7 +11,7 @@ describe('stream', () => { }); test('should notify listener about new value', () => { - let value: number = 0; + let value: number | undefined = undefined; const s = stream(); @@ -73,69 +73,6 @@ describe('stream', () => { expect(s).not.toBe(a); }); - test('should destroy listeners and stream must not respond to new values', () => { - let value; - const s = stream(); - s.listen((v) => (value = v)); - - s.destroy().send(5); - - expect(value).toBeUndefined(); - }); - - test('should invoke destroyed listener before self destroying', () => { - let value; - const s = stream(); - - s.on(StreamEvent.DESTROY, () => (value = 'destroyed')); - - s.destroy(); - - expect(value).toMatch('destroyed'); - }); - - test('freeze method should stop accepting new value', () => { - let value; - const s = stream(); - s.listen((v) => (value = v)); - - s.freeze().send(1); - - expect(value).toBeUndefined(); - }); - - test('should invoke frozen listener before self freezing', () => { - let value; - const s = stream(); - - s.on(StreamEvent.FREEZE, () => (value = 'frozen')); - - s.freeze(); - - expect(value).toMatch('frozen'); - }); - - test('resume should allow stream to pass new values', () => { - let value: number = 0; - const s = stream(); - s.listen((v) => (value = v)); - - s.freeze().resume().send(1); - - expect(value).toBe(1); - }); - - test('should invoke resumed listener before self resume', () => { - let value: string = ''; - const s = stream(); - - s.on(StreamEvent.RESUME, () => (value = 'resumed')); - - s.freeze().resume(); - - expect(value).toMatch('resumed'); - }); - test('concat method should merge this stream with another one', () => { let value: number = 0; @@ -160,16 +97,4 @@ describe('stream', () => { d.send(4); expect(value).toBe(4); }); - - test('uniqueBy method should filter values that sream already passed on', () => { - let value: number = 0; - const s = stream(); - - s.uniqueBy((value) => value).listen((v) => (value = v)); - - s.send(1); - expect(value).toBe(1); - s.send(2).send(1); - expect(value).toBe(2); - }); }); diff --git a/test/tap.spec.ts b/test/tap.spec.ts index d57f8c9..2fbf510 100644 --- a/test/tap.spec.ts +++ b/test/tap.spec.ts @@ -11,7 +11,10 @@ describe('tap', () => { let testVariable = ''; const value = 5; - const effect = async (_number: number) => void (testVariable = 'filled'); + const effect = (_number: number) => + new Promise((resolve) => + setTimeout(() => ((testVariable = 'filled'), resolve())), + ); tap(effect)(value); diff --git a/test/transducer.spec.ts b/test/transducer.spec.ts new file mode 100644 index 0000000..6bc5ffb --- /dev/null +++ b/test/transducer.spec.ts @@ -0,0 +1,59 @@ +import { + map, + List, + filter, + toList, + isList, + toArray, + Foldable, + transduce, +} from '../src'; + +describe('transducer', () => { + it('should filter an array', () => { + const result = transduce([1, 2, 3, 4, 5, 6])(toArray())( + filter((n) => n < 5), + ); + + expect(result).toEqual([1, 2, 3, 4]); + }); + + it('should map an array from number to string', () => { + const result = transduce([1, 2, 3])(toArray())( + map, number, string>(String), + ); + + expect(result).toEqual(['1', '2', '3']); + }); + + it('should combine two operation: filter and map', () => { + const result = transduce([1, 2, 3])(toArray())( + filter, number>((value) => value >= 2), + map, number, string>(String), + ); + + expect(result).toEqual(['2', '3']); + }); + + it('should invoke reduce function of foldable instance only once', () => { + const foldable: Foldable = [1, 2, 3]; + + const reduceSpy = jest.spyOn(foldable, 'reduce'); + + transduce(foldable)(toArray())( + filter, number>((value) => value >= 2), + map, number, string>(String), + ); + + expect(reduceSpy).toBeCalledTimes(1); + }); + + it('should reduce array of numbers to list', () => { + const result = transduce([1, 2, 3])(toList())( + filter, number>((value) => value >= 2), + ); + + expect(isList(result)).toBe(true); + expect(result.asArray()).toEqual([2, 3]); + }); +}); diff --git a/test/tuple.spec.ts b/test/tuple.spec.ts deleted file mode 100644 index b7c260a..0000000 --- a/test/tuple.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { tuple, isTuple, TUPLE_OBJECT_TYPE } from '../src/tuple'; - -describe('tuple', () => { - test('should create tuple from set of values', () => { - expect(typeof tuple(8, 'number')).toBe('object'); - }); - - test('isTuple should return true if value is a Tuple', () => { - expect(isTuple(tuple(8, 'number'))).toBe(true); - expect(isTuple(8)).toBe(false); - }); - - test('should return item from tuple', () => { - expect(tuple(1).item(0)).toBe(1); - }); - - test('should transform value of a tuple', () => { - const newValue = 'transformed'; - expect( - tuple(8) - .transform(0, () => newValue) - .item(0), - ).toBe(newValue); - }); - - test('should get position of a given element or -1 of element is not in tuple', () => { - expect(tuple(false).position(true)).toBe(-1); - expect(tuple(false).position(false)).toBe(0); - }); - - test('should append value to tuple', () => { - expect(tuple(1).append('').size()).toBe(2); - expect(tuple(false).append(5).item(1)).toBe(5); - }); - - test('should prepend value to tuple', () => { - expect(tuple(4).prepend(true).size()).toBe(2); - expect(tuple(5).prepend('').item(0)).toBe(''); - }); - - test('should remove first element from tuple', () => { - expect(tuple(6).shift().size()).toBe(0); - expect(tuple(6, 8, 9).shift().item(0)).toBe(8); - }); - - test('should remove element from end of tuple', () => { - expect(tuple(6).pop().size()).toBe(0); - }); - - test('should get length of tuple', () => { - expect(tuple().size()).toBe(0); - }); - - test('should be iterable and convertible to array', () => { - expect(Symbol.iterator in tuple()).toBe(true); - expect(Array.from(tuple(4))).toEqual([4]); - }); - - test('should be serializable', () => { - expect(tuple(6).toJSON()).toEqual({ type: TUPLE_OBJECT_TYPE, value: [6] }); - }); - - test('should concat another tuple', () => { - expect(tuple(1).concat(tuple(2)).size()).toBe(2); - }); - - test('asArray method should return values of tuple', () => { - expect(tuple(4).asArray()).toEqual([4]); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index c70488e..23b8005 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "strict": true, "target": "ES2018", + "module": "ESNext", "rootDir": "src", "declaration": true, "declarationDir": "build",