0.1.2 • Published 5 years ago

pronad v0.1.2

Weekly downloads
2
License
ISC
Repository
github
Last release
5 years ago
  • Promises must have all the methods, so that async fns can be .mapped
  • chain methods must result in a Pronad so we can track the rejected type
  • :. Pronad must be ambient type so that Promise knows about it
  • Pronad definition should inherit Promise prototype to maintain consency
  • How bout Promise.bind => Promise<Pnd<E, T>>. recover works on resolved promise of Promise<{ left: true, value: T }>, and catch still catches errors
  • in fact why doesn't recover take ((l: E) => T, (err: any) => T)

This module:

  • Extends the native Promise prototype to include monad methods and TypeScript typings.
  • Provides a Pronad<Rej, Res> TypeScript type to annotate the rejected state of a promise

It was built to ease frustrations while trying to bridge the gap when working with native Promises which lacked features and structure around types, and arbitrary monad libraries which lacked async abilities.

(I recommend checking out the Fluture library as another more advanced alternative.)

How to use

An example...

/*** Some function that returns a Promise ***/
function playTheGame(): Pronad<boolean, boolean> { 
  return Math.random() > 0.5
    ? Promise.resolve(true)
    : Promise.reject(false);
}

/*** Another function that returns a Promise ***/
function validateWinner(resVal: string): Pronad<Error, string> {
  return Math.random() > 0.5
    ? Promise.resolve(resVal)
    : Promise.reject(new Error('cheating involved');
}

const competitionResult: Pronad<Error, string> = playTheGame
  .bimap(
    // only runs on rejected
    (rejVal: boolean): Error => new Error('You lost the game'),
    // only runs on resolved
    (resVal: boolean): string => 'Congratulations you won'
  )
  // only runs on resolved, and should return concrete value or resolved promise
  .map(someAddPlayerScoreToStringFn)
  // `validateWinner` only runs on resolved, and returns another Promise / Pronad
  .flatMap(validateWinner);
  // You could keep chaining here instead of assigning to a const

const forRender: string = await competitionResult
  // simply switches the rejected value to same type as resolved so it can be safely awaited
  .recover((rejVal: Error | any): string => typeof rejVal === 'Error'
    ? `Unfortunately you lost: ${rejVal.message}`
    : 'Unfortunately you did not win, but we can\'t tell you why')

The Pronad type is interchangable with Promises, they only offer the extra generic slot for notation of the type of the Promise's rejected value*.

Promise method equivalency

Without delving into monad thoery, here's a cheat-sheet on the methods available:

.map -> .then – for functions which return a concrete value, or intend to return a resolved promise.

.flatMap -> .then – for functions which return a Promise (resolved or rejected).

.rejMap -> .catch – transform your rejected value; the return will automatically be Promise.reject()ed.

.rejFlatMap -> .catch – for functions which return a Promise (resolved or rejected), but is only to be run on rejected state, with the rejected value.

.cata(rejFn, resFn) -> .then(resFn, rejFn) – to resolve to a standardised type for final consumption by the application

.recover -> .catch also serves this purpose, but only runs for rejected state and must return a value the same type as that of the resolved state.

Installation

Install the dependency:

npm i pronad

Call the initialiser

Somewhere in your bootstrapping code, include:

import { monadifyPromises } from 'pronad';

monadifyPromises();

(We'd wanted to keep mutating native prototypes as an opt-in situation :D)

For TypeScript projects:

Wherever you declare your types, create a file:

// pronad.d.ts

import 'pronad/src/ambient';

(or include in your existing type bootstrap)

This adds the monadic methods to the Promise interface.

Caveats / notes on usage

Underlying this library are the inherantly problematic javascript Promises.

.catch catches all errors

Any function used to operate on the rejected side of the promise is evaluated with .catch behind the scenes. This function inherantly must accept an any, since any errors that are thrown (willingly or otherwise) will end up in this block.

Functions used in rejMap, rejFlatMap, cata which deal with the rejected state are annotated with rejFn: (E | any) => ... (which of course, is simply any); the E is given as a indicative only.

You can't wrap Promises

Promises flatten by design. If you try to form a Pronad<E, Pronad<F, T>>, it will flatten to Pronad<F, T>.

Voiding the type contract

If a function throws, or you return a Promise.reject() from a .map, your Pronad will end up in the rejected state, even though the .map by definition should retain the state of the monad.

There's still more exploring to do around how solid the rejected side of the Pronad type is.

Them's the breaks


Todo

  • Experiment with alternative rej typing in the Pronad<E,T>
  • Explore whether recover fn can be optional / default to identity and still error if type is not maintained
  • Write documentation on each method
  • Alternative to Promise.all to convert Array<Pronad<bad, good>> and collect all values into a Pronad<Array<bad>, Array<good>>.
  • Provide an alternative import file to auto-initialise(?)
0.1.2

5 years ago

0.1.1

5 years ago

0.1.0

5 years ago

0.0.3

5 years ago

0.0.2

5 years ago

0.0.1

5 years ago