@fluss/core

Core functions and structures for functional programming.

Usage no npm install needed!

<script type="module">
  import flussCore from 'https://cdn.skypack.dev/@fluss/core';
</script>

README

@fluss/core

code style: prettier

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 ✌️