README
@fluss/core
Library for functional coding in modern environment.
Design goals
- Get the most from TypeScript's inference power.
- The implementation of each function should be as minimal as possible.
- All functions are immutable, and there are no side-effects.
- All functions must be safe as much as possible.
- Do not override native methods, if function will make same work and produce result same as native method.
- Each function is maximally independent module (I try my best, though there can be some dependencies).
Example use
const curriedFn /*: Curried<(args_0: string, args_1: string) => string, 2> */ =
curry((left: string, right: string) => left + right);
const curriedFn2 /*: Curried<(args_0: string) => string, 1> */ = curriedFn('');
const result /*: string */ = curriedFn2('');
@fluss/core's advantages
- TypeScript included out the box
- Small size
- Separated modules. You can import only needed functions into your code.
Install
npm i @fluss/core
Import
import { curry } from '@fluss/core';
// or
import { curry } from '@fluss/core/curry';
API
Library is bundled as bunch of ES modules. It doesn't support CommonJS. If you need old module system, transform code with any tool (Babel
etc.).
In TypeScript's examples is used Flow's comment notation if TypeScript infer type by yourself.
pipe
function pipe<
T extends readonly [
(...args: ReadonlyArray<any>) => any,
...ReadonlyArray<(arg: any) => any>
],
>(
...fns: T
): IsComposable<T> extends false
? never
: (
...args: Parameters<First<T>>
) => NFn.IsAsyncIn<T> extends true
? ReturnType<NArray.Last<T>> extends Promise<infer U>
? Promise<U>
: Promise<ReturnType<NArray.Last<T>>>
: ReturnType<NArray.Last<T>>;
Compose functions from left to right. Can handle asynchronous functions along with synchronous ones.
const fn /*: (str: string) => string */ = pipe(
(str) => str + 2,
(str: string) => str + 3,
);
const result /*: '123' */ = fn('1');
const composed /* Promise<number> */ = pipe(
async (str: string) => str + 2,
parseInt,
);
const asyncResult /*: Promise<number> */ = composed('1');
identity
function identity<T>(value: T): T;
Returns own argument back to the calling place.
const value = 5;
const result /*: 5 */ = identity(value);
once
interface OnceFunction {
<T extends ReadonlyArray<unknown>, R>(fn: (...args: T) => R): (
...args: T
) => Option<R>;
<T extends ReadonlyArray<unknown>, R>(
fn: (...args: T) => R,
after: (...args: T) => R,
): (...args: T) => R;
}
Execute fn only once. And then after function if it is provided.
const doOnlyOnce = once(() => {
/* Initialize something. */
});
flip
function flip<F extends (...args: ReadonlyArray<any>) => any>(
fn: F,
): (...args: NArray.Reverse<Parameters<F>>) => ReturnType<F>;
Reverses function's parameters.
const fn = (s: string, n: number) => Number(s) + n;
const flipped /*: (args_0: number, args_1: string) => number */ = flip(fn);
// ...
flipped(1, '2'); // -> 3
curry
function curry<
F extends (...args: ReadonlyArray<any>) => any,
A extends number = NFn.FixedParametersCount<Parameters<F>>,
>(fn: F, arity?: A): Curried<F, A>;
Create curried version of function with optional partial application. If function accepts variadic arguments (...rest), then you can apparently define function's arity.
const fn /*: Curried<(arg_0: string, arg_1: string) => string, 2> */ = curry(
(str1: string, str2: string) => str1 + str2 + 3,
);
There is a special value _
that you can use with curried function to preserve place for an argument for the next function execution.
// _anotherFn_ will accept first parameter from original _fn_ function.
const anotherFn /*: Curried<(arg_0: string) => string, 1> */ = fn(_, '2');
fork
interface ForkJoinFunction {
<F extends ReadonlyArray<(...args: ReadonlyArray<any>) => any>>(...fns: F): <
R,
>(
join: (...args: NFn.ReturnTypesOf<F>) => R | Promise<R>,
) => (
...args: NFn.IsParametersEqual<F> extends true
? Parameters<NArray.First<F>>
: never
) => NFn.IsAsyncIn<F> extends true
? R extends Promise<unknown>
? R
: Promise<R>
: R;
}
Allow join output of two functions that get the same input and process it in a different way.
// Compute average.
const y /*: (a: Array<number>) => number */ = fork(
(a: Array<number>) => a.reduce((sum, num) => sum + num, 0),
(a: Array<number>) => a.length,
)((sum, count) => sum / count);
demethodize
function demethodize<T extends object, K extends keyof FunctionKeys<T>>(
target: T,
name: K,
): (
...args: Parameters<T[Cast<K, keyof T>]>
) => ReturnType<T[Cast<K, keyof T>]>;
Extracts method from object.
const createElement = demethodize(document, 'createElement');
// ...
const div /*: HTMLElement */ = createElement('div');
binary
interface BinaryOperation {
(operator: '+'): <O extends string | number>(
f: O,
s: O
) => O extends number ? number : string;
(operator: '-' | '/' | '%' | '*' | '**): (f: number, s: number) => number;
(operator: '>' | '<' | '>=' | '<='): (f: number, s: number) => boolean;
(operator: '==='): <O>(f: O, s: O) => boolean;
(operator: '=='): <F, S>(f: F, s: S) => boolean;
(operator: '||' | '&&'): (f: boolean, s: boolean) => boolean;
(operator: string): <O>(f: O, s: O) => [f: O, s: O];
}
Creates function for binary operation. For unknown operator it returns tuple with left and right operands.
const sum = [1, 2, 3, 4, 5, 6].reduce(binary('+'), 0);
sequentially
function sequentially<
V extends ReadonlyArray<(...values: ReadonlyArray<any>) => any>,
>(
...fns: V
): (
...values: If<
NArray.IsSameInnerType<SequentiallyParameters<V>>,
SequentiallyParameters<V> | [NArray.First<SequentiallyParameters<V>>],
SequentiallyParameters<V>
>
) => NFn.IsAsyncIn<V> extends true
? Promise<NFn.ReturnTypesOf<V>>
: NFn.ReturnTypesOf<V>;
Invokes independently functions with their parameters in order that they are declared. Can handle asynchronous functions.
const inSequence /*: (arg_0: number, arg_1: string) => [number, string] */ =
sequentially(
(n: number) => n ** 2,
(s: string) => s + '!',
);
inSequence(1, 'Hello');
concurrently
function concurrently<
F extends ReadonlyArray<(...args: ReadonlyArray<any>) => any>,
>(
...fns: F
): (
...args: If<
NArray.IsSameInnerType<ConcurrentlyParameters<F>>,
ConcurrentlyParameters<F> | [NArray.First<ConcurrentlyParameters<F>>],
ConcurrentlyParameters<F>
>
) => Promise<NFn.ReturnTypesOf<F>>;
Executes functions simultaneously and can return arrays of execution results.
const fn = concurrently(
(n: number) => {
/* Do some work */
},
(n: number, b: boolean) => {
/* Do some other work */
},
);
fn(9, [1, true]).then(([firstResult, secondResult]) => {
/* handle */
});
isNothing
function isNothing<T>(value: T | Nothing): value is Nothing;
Checks if value is null
or undefined
.
const y /*: boolean */ = isNothing(null);
const y1 /*: boolean */ = isNothing(false);
const y2 /*: boolean */ = isNothing(0);
isJust
function isJust<T>(value: T): value is Just<T>;
Checks if value is not null
and undefined
.
const y /*: boolean */ = isJust(null);
const y1 /*: boolean */ = isJust(false);
const y2 /*: boolean */ = isJust(0);
isError
function isError<E extends Error>(
value: any,
childClass?: Constructor<E>,
): value is E;
Checks if value is Error
or its extended classes.
const y /*: false */ = isError(null);
const y1 /*: true */ = isError(new Error('message'));
const y2 /*: true */ = isError(new TypeError('message'), TypeError);
const y2 /*: false */ = isError(new Error('message'), TypeError);
isPromise
function isPromise<T>(value: any): value is Promise<T>;
Checks if value is Promise
.
const y /*: false */ = isPromise(false);
const y1 /*: true */ = isPromise(Promise.resolve(9));
isFunction
function isFunction<F extends Function>(value: unknown): value is F;
Check if value is a function.
const f: unknown = () => 2;
if (isFunction<() => number>(f)) {
// `f` will be type of () => number here.
}
throttle
function throttle<F extends (...args: ReadonlyArray<any>) => void>(
fn: F,
frames?: number,
): F;
Makes function be executed once per frames count. If frames argument is equal to 0
or less, then, if present, requestAnimationFrame
is used. Otherwise, setTimeout
function is in use.
const cpuHeavyFunction = throttle(() => {
/* Do some heavy stuff. */
}, 4);
document.addEventListener('scroll', cpuHeavyFunction);
consequent
interface ConsequentFunction<F extends (...args: ReadonlyArray<any>) => any> {
/** Signals if this function is executing now. */
readonly busy: boolean;
(...args: Parameters<F>): Option<ReturnType<F>>;
}
function consequent<F extends (...args: ReadonlyArray<any>) => any>(
fn: F,
): ConsequentFunction<F>;
Executes function while it is not in process. It can handle asynchronous functions.
const consequentFunction = consequent((...args) => {
/* Some work here */
});
// It returns an `Option` monad as result.
const result = consequentFunction(); // Start doing the job.
consequentFunction(); // If previous invocation is not completed then this is ignored.
debounce
function debounce<
F extends (...args: ReadonlyArray<unknown>) => void | Promise<void>,
>(fn: F, frames = 0): F;
Delays function invocation for frames from last invocation of debounced function. If interval between invocations will be less than frames time, then original function won't be executed.
const debouncedFunction = debounce((event: ScrollEvent) => {
/* Some work here */
}, 2);
// It starts job when you finish scrolling.
window.addEventListener('scroll', debouncedFunction);
delay
function delay<T>(fn: () => T | Promise<T>, frames?: number): Delay<T>;
Lengthens function invocation at some frames. If frames equals to zero or less, then requestAnimationFrame
function is used.
delay(() => {
/* Some work here. */
}); // Will use `requestAnimationFrame` in browser.
const stamp = delay(() => {
/* Some another work here. */
}, 2); // Will use `setTimeout`.
stamp.canceled; // -> false
stamp.result; // -> Promise<T> holds result if delayed function.
stamp.cancel(); // -> cancels delay.
memoize
function memoize<
F extends (...args: ReadonlyArray<any>) => any,
K extends (...args: Parameters<F>) => any = (
...args: Parameters<F>
) => NArray.First<Parameters<F>>,
>(fn: F, keyFrom?: K): WithCache<F, K>;
Wraps function and cache all execution results. Allows to customize key for cache. By default, it is first function's argument. Cache readable object is visible to outside.
const fn = (num: number) => Math.random() * num;
const memoizedFn = memoize(fn);
const result1 = memoizedFn(1); // Function is executed
const result2 = memoizedFn(1); // Value from cache will be returned
const result3 = memoizedFn(4); // Function is executed
// Allows manually clear cache.
memoizedFn.cache.clear();
transduce
function transduce<T extends Foldable<any>>(
instance: T,
): <I, K>(
aggregator: Reducer<I, K>,
) => <R extends ReadonlyArray<Transducer<I, any, any>>>(
...transducers: ChainTransducers<R>
) => I;
Creates transduce operation over a Foldable
instance.
const result /*: readonly string[] */ = transduce([1, 2, 3])(toArray<string>())(
filter<ReadonlyArray<string>, number>((value) => value >= 2),
map<ReadonlyArray<string>, number, string>(String),
);
There are two functions that builds transducers: map
and filter
.
reducer
function reducer<T>(
initial: T,
): <K>(fn: (accumulator: T, current: K) => T) => Reducer<T, K>;
Helps building reducer.
const reduceFunction /*: Reducer<number, number> */ = reducer(0)(binary('+')); // create reducer that sum numbers.
There are two predefined reducers that collect value: toArray
and toList
.
array
function array<T>(
...iterables: ReadonlyArray<T | ArrayLike<T> | Iterable<T>>
): ReadonlyArray<T>;
Creates readonly array from set of ArrayLike, iterable objects or values.
const y /*: ReadonlyArray<number> */ = array(9, new Set([6]), {
0: 6,
length: 1,
});
tryCatch
interface TryCatchFunction {
<T extends ReadonlyArray<any>, L extends Error, R>(
tryFn: (...inputs: T) => R,
): (
...args: T
) => R extends Promise<infer U> ? Promise<Either<L, U>> : Either<L, R>;
<T extends ReadonlyArray<any>, L extends Error, R>(
tryFn: (...inputs: T) => R,
catchFn: (error: L) => R,
): (...args: T) => R;
}
Catches error that may occur in tryFn function. If catchFn is defined, then result will be returned. Otherwise, Either
with an error or result.
const getUser /*: (id: string) => User */ = tryCatch(
(id: string) => getUserFromDbById(id),
(error: NoUserError) => createUser(),
);
tap
function tap<T>(effect: (value: T) => void | Promise<void>): (value: T) => T;
Performs side effect on value while returning it as is.
const result /*: 5 */ = tap(console.log)(5);
freeze
function freeze<T extends object, D extends boolean = false>(
value: T,
deep?: D,
): D extends true ? DeepReadonly<T> : Readonly<T>;
Perform shallow(deep is false
) or deep(deep is true
) freeze of object. By default function does shallow freezing.
const frozenObject /*: Readonly<{ hello: () => void }> */ = freeze({
hello() {
console.log('Hello world');
},
});
const deepFrozenObject /*: DeepReadonly<{ hello: () => void }> */ = freeze(
{
hello() {
console.log('Hello world');
},
},
true,
);
when
interface ConditionalFunction<A extends ReadonlyArray<any>> {
<R>(onTrue: (...values: A) => R): (...values: A) => Option<R>;
<R>(onTrue: (...values: A) => R, onFalse: (...values: A) => R): (
...values: A
) => R;
}
function when<A extends ReadonlyArray<any>>(
condition: (...values: A) => boolean,
): ConditionalFunction<A>;
Replaces conditional flow (ternary operator and if
/else
).
const multiplyIf = when((num: number) => num > 10)((num) => num * 3, identity);
const result /*: number */ = multiplyIf(9); // Will be returned as is.
const result2 /*: number */ = multiplyIf(11); // Will be multiplied.
isOption
function isOption<T>(value: any): value is Option<T>;
Checks if value is instance of Option
monad.
isOption(8); // false
isOption(maybe(8)); // true
maybe
function maybe<T>(
value: T,
): unknown extends T ? Option<T> : T extends Just<T> ? Some<T> : None;
Wraps value with Option
monad. Function detects state (Just or Nothing) of Option
by yourself.
maybe(8); // Some<number>
maybe(null); // None
some
function some<T>(value: T): Some<T>;
Creates Option
monad instance with Just state.
some(2); // Some<number>
none
const none: None;
Option
' monads instance with Nothing state.
const a /*: None */ = none;
Option
Monad that gets rid of null
and undefined
. Its methods works only if inner value is not nothing(null
and undefined
) and its state is Just
, otherwise they aren't invoked (except extract
and fill
). Wraps nullable value and allow works with it without checking on null
and undefined
.
isEither
function isEither<L extends Error, R>(value: any): value is isEither<L, R>;
Checks if value is instance of Either
monad.
isEither(8); // false
isEither(either(8)); // true
right
function right<R>(value: R): Right<R>;
Wraps value with Either
monad with Right state.
// We are sure that 8 is not "left" value.
right(8); // Right<number>
left
function left<L>(value: L): Left<L>;
Creates Either
monad instance with Left state.
left<Error>(new Error('Error is occurred!')); // Left<Error>
either
function either<A, B>(
isRight: (value: A | B) => value is B,
value: A | B,
): Either<A, B>;
Lift value into Either
monad. isRight parameter helps find out if value must belong to Right
or Left
type.
const result /*: Either<Error, string> */ = either(
isString,
new Error('I am a value'),
);
Either
Monad that can contain success value or failure value. Allow handle errors in functional way.
task
function task<T, E extends Error>(
fork: ForkFunction<T, E> | Task<T, E> | Promise<T>,
): Task<T, E>;
Defines Task
or copies fork function from another Task
or Promise
.
function getSomeDataFromInternet(): Promise<string> {
/* useful code */
}
const dataTask = task(getSomeDataFromInternet()).map(JSON.parse);
// somewhere in code
dataTask.start((data) => {
/* do job with data */
});
// or you can convert Task to Promise and expose data
const data = await dataTask.asPromise(); // This method also starts task as `start`.
done
function done<T, E extends Error>(value: T): Task<T, E>;
Wraps value to process as Task
.
const data = {
/* some data */
};
const dataTask = done(data).map(JSON.stringify).chain(task(sendOverInternet));
// somewhere in code
dataTask.start(
() => {
/* on done job */
},
(error) => {
/* on fail job */
},
);
fail
function fail<T, E extends Error>(value: E): Task<T, E>;
Create failed Task
.
const dataTask = fail(someError);
// somewhere in code
dataTask.start(
() => {
/* on done job */
},
(error) => {
/* on fail job */
},
);
isTask
function isTask<T, E extends Error>(value: any): value is Task<T, E>;
Check if value is instance of Task
.
const dataTask = done(8);
isTask(dataTask); // true
Task
Monad that allow to perform some actions asynchronously and deferred in time (in opposite Promise
that starts doing job immediately after definition).
Difference between Task and Promise.
list
function list<T>(
...values: ReadonlyArray<T | ArrayLike<T> | Iterable<T>>
): List<T>;
Create List
from values, array-like objects or iterables.
const numbers /*: List<number> */ = list(1, new Set([2]), [3]);
iterate
function iterate<T>(fn: IteratorFunction<T>): List<T>;
Create List
from function that returns iterator.
const numbers /*: List<number> */ = iterate(function* () {
yield 1;
yield 2;
yield 3;
});
isList
function isList<T>(value: any): value is List<T>;
Checks if value is instance of List
.
const result /*: boolean */ = isList(list());
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.
stream
function stream<T>(): Stream<T>;
Creates live empty stream.
const y /*: Stream<number> */ = stream<number>();
y.map((value) => Math.pow(value, 2)).listen(
(value) => (document.body.innerHTML = value),
);
// Somewhere in the code
y.send(2); // document.body.innerHTML will set to equal to 4
Stream
Structure that makes operations with values over time in live mode.
idle
function idle<T>(fn: () => T): Idle<T>;
Queues a data returned by fn
to be evaluated at interpreter's idle period.
const value /*: Idle<boolean> */ = idle(() => 1).map((num) => num > 7);
// somewhere in the code
const evaluated /*: boolean */ = value.extract();
Idle
Monad that allow to defer data initialization.
reviver
function reviver(
key: string,
value: JSONValueTypes | SerializabledObject<any>,
): JSONValueTypes | List<any> | Idle<any> | Option<any> | Either<Error, any>;
Add recognition of Idle
, Option
, List
, Either
data structures for JSON.parse
.
const obj = JSON.parse('{"type":"Some","value":1}', reviver);
// obj will be instance of Option type.
Word from author
Have fun ✌️