Skip to content

critocrito/dashp

Repository files navigation

DashP

Utilities for monadic promises.

Synopsis

License: GPL v3 npm version GitHub Workflow Status Coverage Status

DashP allows to program with Promises in a functional style. It offers a collection of higher-order and utility functions that operate on Promises and are all curried. It feels similar to lodash/fp, but then for promises.

It implements an (almost) monadic interface for Promises, but unlike other great libraries it doesn't introduce new semantics. It just forms a small wrapper around the native Promise API. This is great for integrating it into any codebase that already uses Promises, without having to relearn new semantics or changing the structure. It retains as well the eager execution semantics of native Promises.

This library intends to be very lightweight. It has no external dependencies and has a size of 3-4K when minified and gzipped.

import {getJson, storeDb} from './async-utils';
import {flowP, tapP} from 'dashp';

const url = "https://url.horse/api";

const apiCall = flowP([getJson, tapP(console.log), storeDb]);

await apiCall(url);

Interoperability

Promises/A+

DashP is compatible with Promises/A+ and ES6 Promises. It also implements Static Land Functor, Bifunctor, Apply, Applicative, Chain and Monad.

As Avaq points out in #1, Promises in their current implementation can't be real Applicative Functors. If a Promise holds another Promise, it automatically assimilates it's value. The then interface acts as map and flatMap at the same time. Therefore dashp is cheating on the precise semantics of Applicatives.

Contents

Usage

npm install --save dashp

Every function has an alias that appends P to the function name, e.g. flowP is an alias for flow and collectP3 is an alias for collect3. This allows for cleaner imports in situations where function names can clash.

import {map, sum} from "lodash/fp";
import {mapP} from "dashp";

map(sum, [1, 2, 3]); // Lodash version.
mapP(sum, [1, 2, 3]); // dashp version.

DashP depends on Array.isArray. You may need to polyfill it if your JavaScript environment doesn't provide it.

API

Creating new Promises
Transforming and combining Promises
Collections
Utility functions

of

Lift a value into a promise.

of :: b -> Promise a b

This is equivalent to Promise.resolve. It returns a promise that resolves to the applied value. This function is compliant with the Static Land Applicative specification.

import {of} from "dashp";

const p = of(23);

p.then(x => console.log(`${x} things.`));
// Prints '23 things.'

reject

Create a rejected promise.

reject :: Promise p => a -> p a b

This function can either take an Error object or an string. If a string is provided, it is converted to an Error.

import {reject} from "dashp";

const msg = "Boom!";

reject(msg).catch(console.log);
// Prints `Error`
reject(new TypeError(msg)).catch(console.log);
// Prints `TypeError`

map

Map a function over a promise.

map :: Promise p => (a -> b) -> p a -> p b

It transforms the value that a promise resolves to and returns a new promise. This is equivalent to promise.then(x => x + 1). The transformation is only applied if the promise resolves successfully, it is ignored if the promise gets rejected. This function is compliant with the Static Land Functor specification.

import {of, map} from "dashp";

const p = of(1);
const f = x => x + 1;

map(f, p).then(console.log);
// Prints 2

bimap

Map either the left or right function over a promise.

bimap :: Promise p => (a -> c) -> (b -> d) -> p a b -> p c d

Map the left function over the rejection value, and the right function over the success value of a promise. This function is compliant with the Static Land Bifunctor specification.

import {of, bimap} from "dashp";

const f = () => console.log('Boom!');
const g = x => x + 1;

bimap(f, g, of(1)).then(console.log);
// Prints 2
bimap(f, g, Promise.reject());
// Prints 'Boom!'

ap

Apply a function wrapped in a promise to a promisified value.

ap :: Promise p => p (a -> b) -> p a -> p b

This function is compliant with the Static Land Apply specification.

import {of, ap} from "dashp";

const pf = of(v => v + 1);
const p = of(1);

ap(pf, p).then(console.log);
// Prints 2

chain

Map a function over a promise.

chain :: Promise p => (a -> p b) -> p a -> p b

This is equivalent to promise.then(f). In practice chain works the same as map since Promises can't be real Applicative Functors. This function is compliant with the Static Land Chain specification.

import {of, chain} from "dashp";

const f = x => of(x + 1);

chain(f, of(0)).then(consol.log);
// Prints 1

compose

Compose two functions that return promises.

compose :: Promise p => (a -> p b) -> (b -> p c) -> p a -> p c

compose yields a third function that returns a promise. The resulting composite function is denoted g∘f : X → Z, defined by (g∘f)(x) = g(f(x)) for all x in X.

import {of, compose} from "dashp";

const f = x => of(x + 1);
const g = x => of(x + 5);
const h = compose(f, g);

h(10).then(console.log);
// Prints 16

whenElse

Branch left if the predicate holds, otherwise branch right.

whenElse :: Promise p => (p a -> Boolean) -> (p a -> p b) -> (p a -> p b) -> p c

This is a conditional branch like the builtin if ... else construct. The predicate, consequent and alternative functions can either return a value or a Promise.

import {whenElse} from "dashp";

const predicate = userExists;
const consequent = updateUser;
const alternative = createUser;

whenElse(predicate, consequent, alternative, user);
// Calls updateUser if the user exists, and otherwise creates it

when

Conditionally call a function if the predicate holds.

when :: Promise p => (p a -> Boolean) -> (p a -> p b) -> p c

This is a conditional branch like the builtin if construct. If the predicate returns true, it will return the result of the consequent, otherwise it returns the original value. The predicate and consequent functions can either return a value or a Promise.

import {when} from "dashp";

const pred = userExists;
const consequent = updateUser;

when(predicate, consequent, user);
// Calls updateUser if the user exists, otherwise returns the user

unlessElse

Branch left if the predicate doesn't hold, otherwise branch right.

unlexxElse :; Promise p => (p a -> Boolean) -> (p a -> p b) -> (p a -> p b) -> p c

This is a conditional branch like the builtin if (! ... ) ... else construct. The predicate, consequent and alternative functions can either return a value or a Promise.

import {unlessElse} from "dashp";

const predicate = userExists;
const consequent = createUser;
const alternative = createUser;

unlessEles(predicate, consequent, alternative, user);
// Creates the user unless it exists, otherwise updates it

unless

Conditionally call a function if the predicate doesn't hold.

unless :: Promise p => (p a -> Boolean) -> (p a -> p b) -> p c

This is a conditional branch like the builtin if (! ...) construct. If the predicate returns false, it will return the result of the consequent, otherwise it returns the original value. The predicate and consequent functions can either return a value or a Promise.

import {unless} from "dashp";

const pred = userExists;
const consequent = createUser;

unless(predicate, consequent, user);
// Calls createUser if the user doesn't exist, otherwise returns the user

all

Resolve all promises in an array.

all :: Promise p => [p b a] -> p b [a]

This is equivalent to Promise.all, with the difference that it creates a callable function.

import {all} from "dashp";

const f = all([openFile1(), opeFile2(), openFile3()]);

f().then(console.log);
// Prints [a, b, c]

fold

Reduce a list of values to a single value, using a reduction function.

fold :: Promise p => (p b c -> p b a -> p b c) -> p b c -> [p b a] -> p b c

This is equivalent to Array.reduce.

import {of, fold} from "dashp";

const f = (acc, x) => of(acc + x);
const xs = [...Array(5).keys()];

fold(f, 0, xs).then(console.log);
// Prints 10

collect

Map a function over every element of a list.

collect :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This is equivalent to Array.map. In it's standard version it only resolves one promise at a time.

import {of, collect} from "dashp";

const f = x => of(x + 1);
const xs = [...Array(5).keys()];

collect(f, xs).then(console.log);
// Prints [1, 2, 3, 4, 5]

collect2

Map a function over every element of a list, resolve two promises in parallel.

collect :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This functions works like collect, with the only difference that two promises are resolved at the same time.

collect3

Map a function over every element of a list, resolve three promises in parallel.

collect3 :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This functions works like collect, with the only difference that three promises are resolved at the same time.

collect4

Map a function over every element of a list, resolve four promises in parallel.

collect4 :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This functions works like collect, with the only difference that four promises are resolved at the same time.

collect5

Map a function over every element of a list, resolve five promises in parallel.

collect5 :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This functions works like collect, with the only difference that five promises are resolved at the same time.

collect6

Map a function over every element of a list, resolve six promises in parallel.

collect6 :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This functions works like collect, with the only difference that six promises are resolved at the same time.

collect7

Map a function over every element of a list, resolve seven promises in parallel.

collect7 :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This functions works like collect, with the only difference that seven promises are resolved at the same time.

collect8

Map a function over every element of a list, resolve eight promises in parallel.

collect8 :: Promise p => (p b a -> p b a) -> [p b a] -> p b [a]

This functions works like collect, with the only difference that eight promises are resolved at the same time.

flatmap

Map a function over every element of a list and concatenate the results.

flatmap :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to calling collect and flattening the resulting list of lists into a single list. In it's standard version it only resolves one promise at a time.

import {flatmap} from "dashp";

const f = x => [x, x];
const xs = [1, 2];

flatmap(f, xs).then(console.log);
// Prints [1, 1, 2, 2]

flatmap2

Map a function over every element of a list and concatenate the results, resolve two promises at the same time.

flatmap2 :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to flatmap, only that it resolves two promises in parallel.

flatmap3

Map a function over every element of a list and concatenate the results, resolve three promises at the same time.

flatmap3 :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to flatmap, only that it resolves three promises in parallel.

flatmap4

Map a function over every element of a list and concatenate the results, resolve four promises at the same time.

flatmap4 :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to flatmap, only that it resolves four promises in parallel.

flatmap5

Map a function over every element of a list and concatenate the results, resolve five promises at the same time.

flatmap5 :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to flatmap, only that it resolves five promises in parallel.

flatmap6

Map a function over every element of a list and concatenate the results, resolve six promises at the same time.

flatmap6 :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to flatmap, only that it resolves six promises in parallel.

flatmap7

Map a function over every element of a list and concatenate the results, resolve seven promises at the same time.

flatmap7 :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to flatmap, only that it resolves seven promises in parallel.

flatmap8

Map a function over every element of a list and concatenate the results, resolve eight promises at the same time.

flatmap8 :: Promise p => (p b a -> p b [a]) -> [p b a] -> p b [a]

This is equivalent to flatmap, only that it resolves eight promises in parallel.

isPromise

Determine whether an object is a promise.

isPromise :: a -> Boolean
import {of, isPromise} from "dashp";

const p = of(23);

isPromise(p);
// Prints true

tap

Call a function for side effect and return the original value.

tap :: Promise p => (p b a -> ()) -> p b a -> p b a
import {of, flow, tap} from "dashp";

const f = a => of(a);

flow([f, tap(console.log)])(23);
// Print "23"

tapClone

Call a function for side effect and return the original value.

tap :: Promise p => (p b a -> ()) -> p b a -> p b a

This function is like tap, but makes a deep clone of the value before applying it to the function.

import {of, flow, tapClone} from "dashp";

const f = a => of(a);

flow([f, tapClone(console.log)])(23);
// Print "23"

caught

Catch an exception on a promise and call a handler.

caught :: Promise p => (p b -> p b a) -> p b -> p b a

This is equivalent to Promise.catch.

import {caught, flow} from "dashp";

const f = () => new Error("Boom");

flow([f, caught(console.err)]);
// Prints the exception

spread

Call a variadic function with the value of a promise as it's arguments.

spread :: Promise p => (a -> b) -> p b [a] -> p b a

If the value is an array, flatten it to the formal parameters of the fulfillment handler.

import {of, flow, spread} from "dashp";

const plus = (x, y) => x + y;
const p = of([1, 2]);

spread(plus, p).then(console.log);
// Prints 3

flow

Compose functions into a chain.

flow :: Promise p => [(a -> c)] -> p b a -> p b c

Create a function out of a list of functions, where each successive invocation is supplied the return value of the previous function call. The new function forms a pipe where the results flow from left to right so to speak. This is equivalent to Lodash's flow function. It's a shortcut for composing more than two functions.

import {of, flow} from "dashp";

const f = (x) -> (y) => of(x + y);
const fs = [...Array(5).keys()].map(f);

flow(fs, 0).then(console.log);
// Prints 10

flow treats any occurrence of caught as a special case by rewriting the function chains to wrap relevant parts in an exception handler. In order to support a syntax like:

import {flow, caught} from "dashp";

const boom = () => { throw new Error; };
const notBoom = () => 23;

flow([
  boom,
  notBoom,
  caught(console.error),
  notBoom,
]);

flow will parse the function chain for any occurrence of caught and rewrite the function chain accordingly to look like this:

flow([
  x => caught(console.error, flow([boom, notBoom], x)),
  notBoom,
]);

flow2

Lift a composed function chain over two arguments.

flow2 :: Promise p => [(a -> a -> c) (c -> d)] -> p b a -> p b a -> p b d

This function works like flow, but it accepts two arguments, that are lifted into the first function of the chain.

flow3

Lift a composed function chain over three arguments.

flow3 :: Promise p => [(a -> a -> a -> c) (c -> d)] -> p b a -> p b a -> p b a -> p b d

This function works like flow, but it accepts three arguments, that are lifted into the first function of the chain.

flow4

Lift a composed function chain over four arguments.

flow4 :: Promise p => [(a -> a -> a -> a -> c) (c -> d)] -> p b a -> p b a -> p b a -> p b a -> p b d

This function works like flow, but it accepts four arguments, that are lifted into the first function of the chain.

flow5

Lift a composed function chain over five arguments.

flow5 :: Promise p => [(a -> a -> a -> a -> a -> c) (c -> d)] -> p b a -> p b a -> p b a -> p b a -> p b a -> p b d

This function works like flow, but it accepts five arguments, that are lifted into the first function of the chain.

flow6

Lift a composed function chain over six arguments.

flow6 :: Promise p => [(a -> a -> a -> a -> a -> a -> c) (c -> d)] -> p b a -> p b a -> p b a -> p b a -> p b a -> p b a -> p b d

This function works like flow, but it accepts six arguments, that are lifted into the first function of the chain.

flow7

Lift a composed function chain over seven arguments.

flow7 :: Promise p => [(a -> a -> a -> a -> a -> a -> a -> c) (c -> d)] -> p b a -> p b a -> p b a -> p b a -> p b a -> p b a -> p b a -> p b d

This function works like flow, but it accepts seven arguments, that are lifted into the first function of the chain.

flow8

Lift a composed function chain over eight arguments.

flow8 :: Promise p => [(a -> a -> a -> a -> a -> a -> a -> a -> c) (c -> d)] -> p b a -> p b a -> p b a -> p b a -> p b a -> p b a -> p b a -> p b a -> p b d

This function works like flow, but it accepts eight arguments, that are lifted into the first function of the chain.

constant

Create a function that always returns the same value.

constant :: a -> (b -> Promise a)
import {constant} from "dashp";

const f = constant("Hello");

f().then(console.log);
// Prints "Hello"

lift2

Lift a binary function over two promises.

lift2 :: Promise p => (a -> a -> a) -> p b a -> p b a -> p b a
import {of, lift2} from "dashp";

const f = (x, y) => x + y;

lift2(f, of(1), of(2)).then(console.log);
// Prints 3

lift3

Lift a ternary function over three promises.

lift3 :: Promise p => (a -> a -> a -> a) -> p b a -> p b a -> p b a -> p b a
import {of, lift3} from "dashp";

const f = (x, y, z) => x + y + z;

lift3(f, of(1), of(2), of(3)).then(console.log);
// Prints 6

lift4

Lift a quartary function over four promises.

lift4 :: Promise p => (a -> a -> a -> a -> a) -> p b a -> p b a -> p b a -> p b a -> p b a
import {of, lift4} from "dashp";

const f = (w, x, y, z) => w + x + y + z;

lift4(f, of(1), of(2), of(3), of(4)).then(console.log);
// Prints 10

delay

Delay the resolution of a promise chain.

delay :: Promise p => x -> p b a -> p b a

The first arguments is the delay in milliseconds.

import {of, delay} from "dashp";

delay(100, of(23)).then(console.log);
// Waits 100 ms and print 23.

retry

Call an action, and retry it in case it fails.

retry :: Promise p => p b a -> p b a

An action is retried up to five times with an increasing timeout. The action can be a function as well. In it's standard version, the action function doesn't receive any arguments.

import {retry} from "dashp";

// Retries `fetchUser` in case of failure.
retry(fetchUser).then(console.log).catch(console.error);

License

GPL 3.0 licensed