Skip to content

Latest commit

 

History

History
1892 lines (1369 loc) · 57.7 KB

README.md

File metadata and controls

1892 lines (1369 loc) · 57.7 KB

Fluture

Build Status Code Coverage Dependency Status NPM Package Gitter Chat Questions and Answers

Fluture offers a control structure similar to Promises, Tasks, Deferreds, and what-have-you. Let's call them Futures.

Much like Promises, Futures represent the value arising from the success or failure of an asynchronous operation (I/O). Though unlike Promises, Futures are lazy and adhere to the monadic interface.

Some of the features provided by Fluture include:

For more information:

Usage

npm install --save fluture

On older environments you may need to polyfill one or more of the following functions: Object.create, Object.assign and Array.isArray.

EcmaScript Module

Fluture is written as modular JavaScript (.mjs). It can be loaded directly by Node 9 and up using --experimental-modules, with the esm loader, or with TypeScript (typings included).

Besides the module system, no other ES5+ features are used in Fluture's source, which means that no transpilation is needed after concatenation.

import {readFile} from 'fs';
import {node, encase} from 'fluture';

var getPackageName = file =>
  node(done => { readFile(file, 'utf8', done) })
  .chain(encase(JSON.parse))
  .map(x => x.name);

getPackageName('package.json')
.fork(console.error, console.log);
//> "fluture"

CommonJS Module

Although the Fluture source uses the EcmaScript module system, versions downloaded from the npm registry include a CommonJS build, which will automatically be used when loading Fluture with require.

var fs = require('fs');
var Future = require('fluture');

var getPackageName = function(file){
  return Future.node(function(done){ fs.readFile(file, 'utf8', done) })
  .chain(Future.encase(JSON.parse))
  .map(function(x){ return x.name });
};

getPackageName('package.json')
.fork(console.error, console.log);
//> "fluture"

Global Bundle (CDN)

Fluture is hosted in full with all of its dependencies at https://cdn.jsdelivr.net/gh/fluture-js/[email protected]/dist/bundle.js

This script will add Fluture to the global scope.

Interoperability

Fantasy Land Static Land

  • Future implements Fantasy Land and Static Land -compatible Alt, Bifunctor, Monad, and ChainRec (of, ap, alt, map, bimap, chain, chainRec). All versions of Fantasy Land are supported.
  • Future.Par implements Fantasy Land 3 and Static Land -compatible Alternative (of, zero, map, ap, alt).
  • The Future and ConcurrentFuture representatives contain @@type properties for Sanctuary Type Identifiers.
  • The Future and ConcurrentFuture instances contain @@show properties for Sanctuary Show.

Butterfly

The name "Fluture" is a conjunction of "FL" (the acronym to Fantasy Land) and "future". Fluture means butterfly in Romanian: A creature one might expect to see in Fantasy Land.

Credit goes to Erik Fuente for styling the logo, and WEAREREASONABLEPEOPLE for sponsoring the project.

Documentation

Table of contents

General
Creating new Futures
Converting between Nodeback APIs and Futures
Converting between Promises and Futures
Transforming and combining Futures
Consuming/forking Futures
Concurrency related utilities and data structures
Resource management
Other utilities

Type signatures

The various function signatures are provided in a small language referred to as Hindley-Milner notation. Read about Hindley-Milner in JavaScript here. On top of the basic Hindley-Milner notation, we use a few additions to describe the JavaScript-specific stuff, like methods or functions that take multiple arguments at once.

Squiggly Arrows

In order to document methods, we use the squiggly arrow (~>). This separates the implicit this argument from the other, explicit, arguments. For example, the following line signifies a method, as indicated by the squiggly arrow:

Future.prototype.map :: Future a b ~> (b -> c) -> Future a c

For comparison, the following example shows a regular function:

map :: (b -> c) -> Future a b -> Future a c

Brackets

Most functions exposed by Fluture are curried. This is reflected in their type signatures by using an arrow at each step where partial application is possible. For example, the following line signifies a curried function, because it has an arrow after each function argument:

add :: Number -> Number -> Number

We could have chosen to write the above line with "groups of one argument", but we usually leave the grouping brackets out for brevity:

add :: (Number) -> (Number) -> Number

In order to document functions and methods that are not curried, we use grouping to show which arguments have to be provided at the same time:

add :: (Number, Number) -> Number

Types

The concrete types you will encounter throughout this documentation:

  • Future - Instances of Future provided by compatible versions of Fluture.
  • ConcurrentFuture - Concurrified Futures (Future.Par).
  • Promise - Values which conform to the Promises/A+ specification.
  • Nodeback a b - A Node-style callback; A function of signature (a | Nil, b) -> x.
  • Pair a b - An array with exactly two elements: [a, b].
  • Iterator - Objects with next-methods which conform to the Iterator protocol.
  • Cancel - The nullary cancellation functions returned from computations.
  • Catchable e f - A function f which may throw an exception e.
  • List - Fluture's internal linked-list structure: { head :: Any, tail :: List }.
  • Context - Fluture's internal debugging context object: { tag :: String, name :: String, stack :: String }.

Type classes

Some signatures contain constrained type variables. Generally, these constraints express that some value must conform to a Fantasy Land-specified interface.

Cancellation

Cancellation is a system whereby running Futures get an opportunity stop what they're doing and release resources that they were holding, when the consumer indicates it is no longer interested in the result.

To cancel a Future, it must be unsubscribed from. Most of the consumption functions return an unsubscribe function. Calling it signals that we are no longer interested in the result. After calling unsubscribe, Fluture guarantees that our callbacks will not be called; but more importantly: a cancellation signal is sent upstream.

The cancellation signal travels all the way back to the source (with the exception of cached Futures - see cache), allowing all parties along the way to clean up.

With the Future constructor, we can provide a custom cancellation handler by returning it from the computation. Let's see what this looks like:

// We use the Future constructor to create a Future instance.
var eventualAnswer = Future(function computeTheAnswer(rej, res){

  // We give the computer time to think about the answer, which is 42.
  var timeoutId = setTimeout(res, 60000, 42);

  // Here is how we handle cancellation. This signal is received when nobody
  // is interested in the answer any more.
  return function onCancel(){
    // Clearing the timeout releases the resources we were holding.
    clearTimeout(timeoutId);
  };

});

// Now, let's fork our computation and wait for an answer. Forking gives us
// the unsubscribe function.
var unsubscribe = eventualAnswer.fork(console.error, console.log);

// After some time passes, we might not care about the answer any more.
// Calling unsubscribe will send a cancellation signal back to the source,
// and trigger the onCancel function.
unsubscribe();

Many natural sources in Fluture have cancellation handlers of their own. after, for example, does exactly what we've done just now: calling clearTimeout.

Finally, Fluture unsubscribes from Futures that it forks for us, when it no longer needs the result. For example, both Futures passed into race are forked, but once one of them produces a result, the other is unsubscribed from, triggering cancellation. This means that generally, unsubscription and cancellation is fully managed for us behind the scenes.

Stack safety

Fluture interprets our transformations in a stack safe way. This means that none of the following operations result in a RangeError: Maximum call stack size exceeded:

var add1 = x => x + 1;
var m = Future.of(1);

for(var i = 0; i < 100000; i++){
  m = m.map(add1);
}

m.fork(console.error, console.log);
//> 100001
var m = (function recur(x){
  var mx = Future.of(x + 1);
  return x < 100000 ? mx.chain(recur) : mx;
}(1));

m.fork(console.error, console.log);
//> 100001

To learn more about memory and stack usage under different types of recursion, see (or execute) scripts/test-mem.

Debugging

First and foremost, Fluture type-checks all of its input and throws TypeErrors when incorrect input is provided. The messages they carry are designed to provide enough insight to figure out what went wrong.

Secondly, Fluture catches exceptions that are thrown asynchronously, and exposes them to you in one of two ways:

  1. By throwing an Error when it happens.
  2. By calling your exception handler with an Error.

The original exception isn't used because it might have been any value. Instead, a regular JavaScript Error instance whose properties are based on the original exception is created. Its properties are as follows:

  • name: Always just "Error".
  • message: The original error message, or a message describing the value.
  • reason: The original value that was caught by Fluture.
  • context: A linked list of "context" objects. This is used to create the stack property, and you generally don't need to look at it. If debug mode is not enabled, the list is always empty.
  • stack: The stack trace of the original exception if it had one, or the Error's own stack trace otherwise. If debug mode (see below) is enabled, additional stack traces from the steps leading up to the crash are included.
  • future: The instance of Future that was being consumed when the exception happened. Often printing it as a String can yield useful information. You can also try to consume it in isolation to better identify what's going wrong.

Finally, as mentioned, Fluture has a debug mode wherein additional contextual information across multiple JavaScript ticks is collected, included as an extended "async stack trace" on Errors, and exposed on Future instances.

Debug mode can have a significant impact on performance, and uses up memory, so I would advise against using it in production.

Sanctuary

When using this module with Sanctuary Def (and Sanctuary by extension) one might run into the following issue:

var S = require('sanctuary');
var Future = require('fluture');
S.I(Future.of(1));
//! Since there is no type of which all the above values are members,
//! the type-variable constraint has been violated.

This happens because Sanctuary Def needs to know about the types created by Fluture to determine whether the type-variables are consistent.

To let Sanctuary know about these types, we can obtain the type definitions from fluture-sanctuary-types and pass them to S.create:

var {create, env} = require('sanctuary');
var {env: flutureEnv} = require('fluture-sanctuary-types');
var Future = require('fluture');

var S = create({checkTypes: true, env: env.concat(flutureEnv)});

S.I(Future.of(1));
//> Future.of(1)

Casting Futures

Sometimes we may need to convert one Future to another, for example when the Future was created by another package, or an incompatible version of Fluture.

When isFuture returns false, a conversion is necessary. Usually the most concise way of doing this is as follows:

var NoFuture = require('incompatible-future');
var incompatible = NoFuture.of('Hello');

//Cast the incompatible Future to our version of Future:
var compatible = Future(incompatible.fork.bind(incompatible));

compatible.both(Future.of('world')).value(console.log);
//> ["Hello", "world"]

Creating Futures

Future

Future :: ((a -> Undefined, b -> Undefined) -> Cancel) -> Future a b
Future :: ((a -> Undefined, b -> Undefined) -> Cancel) -> Future a b

Creates a Future with the given computation. A computation is a function which takes two callbacks. Both are continuations for the computation. The first is reject, commonly abbreviated to rej; The second is resolve, or res. When the computation is finished (possibly asynchronously) it may call the appropriate continuation with a failure or success value.

Additionally, the computation may return a nullary function containing cancellation logic. See Cancellation.

Future(function computation(reject, resolve){
  setTimeout(resolve, 3000, 'world');
});

of

of :: b -> Future a b
of             :: b -> Future a b
resolve        :: b -> Future a b
Future.of      :: b -> Future a b
Future.resolve :: b -> Future a b

Creates a Future which immediately resolves with the given value.

This function has an alias resolve.

var eventualThing = Future.of('world');
eventualThing.fork(
  console.error,
  thing => console.log(`Hello ${thing}!`)
);
//> "Hello world!"

reject

reject :: a -> Future a b
reject        :: a -> Future a b
Future.reject :: a -> Future a b

Creates a Future which immediately rejects with the given value.

var eventualFailure = Future.reject('I got so far!');
eventualFailure.fork(
  e => console.error('I tried so hard!', e),
  console.log
);
//! "I tried so hard! I got so far!"

after

after :: Number -> b -> Future a b
after :: Number -> b -> Future a b

Creates a Future which resolves with the given value after the given number of milliseconds.

var eventualThing = Future.after(500, 'world');
eventualThing.fork(console.error, thing => console.log(`Hello ${thing}!`));
//> "Hello world!"

rejectAfter

rejectAfter :: Number -> a -> Future a b
rejectAfter :: Number -> a -> Future a b

Creates a Future which rejects with the given reason after the given number of milliseconds.

var eventualError = Future.rejectAfter(500, new Error('Kaputt!'));
eventualError.fork(err => console.log('Oh no - ' + err.message), console.log);
//! Oh no - Kaputt!

do

do :: (() -> Iterator) -> Future a b
do :: (() -> Iterator) -> Future a b
go :: (() -> Iterator) -> Future a b

A way to do async/await with Futures, similar to Promise Coroutines or Haskell Do-notation.

Takes a function which returns an Iterator, commonly a generator-function, and chains every produced Future over the previous.

This function has an alias go, for environments where do is reserved.

var eventualMessage = Future.do(function*(){
  var thing = yield Future.after(300, 'world');
  var message = yield Future.after(300, 'Hello ' + thing);
  return message + '!';
});

eventualMessage.fork(console.error, console.log);
//After 600ms:
//> "Hello world!"

To handle errors inside a do procedure, we need to fold the error into our control domain, I recommend folding into an Either:

var attempt = Future.fold(S.Left, S.Right);
var ajaxGet = url => Future.reject('Failed to load ' + url);

var eventualMessage = Future.do(function*(){
  var e = yield attempt(ajaxGet('/message'));
  return S.either(
    e => `Oh no! ${e}`,
    x => `Yippee! ${x}`,
    e
  );
});

eventualMessage.fork(console.error, console.log);
//> "Oh no! Failed to load /message"

try

try :: Catchable e (() -> r) -> Future e r
try     :: Catchable e (() -> r) -> Future e r
attempt :: Catchable e (() -> r) -> Future e r

Creates a Future which resolves with the result of calling the given function, or rejects with the error thrown by the given function.

Short for Future.encase(f, undefined).

This function has an alias attempt, for environments where try is reserved.

var data = {foo: 'bar'};
Future.try(() => data.foo.bar.baz)
.fork(console.error, console.log);
//> [TypeError: Cannot read property 'baz' of undefined]

tryP

tryP :: (() -> Promise e r) -> Future e r
tryP :: (() -> Promise e r) -> Future e r

Create a Future which when forked spawns a Promise using the given function and resolves with its resolution value, or rejects with its rejection reason.

Short for Future.encaseP(f, undefined).

Future.tryP(() => Promise.resolve('Hello'))
.fork(console.error, console.log);
//> "Hello"

node

node :: (Nodeback e r -> x) -> Future e r
node :: (Nodeback e r -> x) -> Future e r

Creates a Future which rejects with the first argument given to the function, or resolves with the second if the first is not present.

Note that this function does not support cancellation.

Short for Future.encaseN(f, undefined).

Future.node(done => {
  done(null, 'Hello');
})
.fork(console.error, console.log);
//> "Hello"

encase

encase :: (Catchable e (a -> r)) -> a -> Future e r
encase  :: (Catchable e ((a      ) -> r)) -> a ->           Future e r
encase2 :: (Catchable e ((a, b   ) -> r)) -> a -> b ->      Future e r
encase3 :: (Catchable e ((a, b, c) -> r)) -> a -> b -> c -> Future e r

Takes a function and a value, and returns a Future which when forked calls the function with the value and resolves with the result. If the function throws an exception, it is caught and the Future will reject with the exception:

Partially applying encase with a function f allows us to create a "safe" version of f. Instead of throwing exceptions, the encased version always returns a Future when given the remaining argument(s):

Furthermore; encase2 and encase3 are binary and ternary versions of encase, applying two or three arguments to the given function respectively.

var data = '{"foo" = "bar"}';
var safeJsonParse = Future.encase(JSON.parse);
safeJsonParse(data).fork(console.error, console.log);
//! [SyntaxError: Unexpected token =]

encaseP

encaseP :: ((a) -> Promise e r) -> a -> Future e r
encaseP  :: ((a) ->       Promise e r) -> a ->           Future e r
encaseP2 :: ((a, b) ->    Promise e r) -> a -> b ->      Future e r
encaseP3 :: ((a, b, c) -> Promise e r) -> a -> b -> c -> Future e r

Allows Promise-returning functions to be turned into Future-returning functions.

Takes a function which returns a Promise, and a value, and returns a Future. When forked, the Future calls the function with the value to produce the Promise, and resolves with its resolution value, or rejects with its rejection reason.

Furthermore; encaseP2 and encaseP3 are binary and ternary versions of encaseP, applying two or three arguments to the given function respectively.

var fetchf = Future.encaseP(fetch);

fetchf('https://api.github.com/users/Avaq')
.chain(res => Future.tryP(_ => res.json()))
.map(user => user.name)
.fork(console.error, console.log);
//> "Aldwin Vlasblom"

encaseN

encaseN :: ((a, Nodeback e r) -> x) -> a -> Future e r
encaseN  :: ((a,       Nodeback e r) -> x) -> a ->           Future e r
encaseN2 :: ((a, b,    Nodeback e r) -> x) -> a -> b ->      Future e r
encaseN3 :: ((a, b, c, Nodeback e r) -> x) -> a -> b -> c -> Future e r

Allows continuation-passing-style functions to be turned into Future-returning functions.

Takes a function which accepts as its last parameter a Nodeback, and a value, and returns a Future. When forked, the Future calls the function with the value and a Nodeback and resolves the second argument passed to the Nodeback, or or rejects with the first argument.

Furthermore; encaseN2 and encaseN3 are binary and ternary versions of encaseN, applying two or three arguments to the given function respectively.

var fs = require('fs');

var read = Future.encaseN2(fs.readFile);

read('README.md', 'utf8')
.map(text => text.split('\n'))
.map(lines => lines[0])
.fork(console.error, console.log);
//> "# [![Fluture](logo.png)](#butterfly)"

chainRec

Future.chainRec :: ((a -> Next a, b -> Done b, a) -> Future e (Next a | Done b), a) -> Future e b
Future.chainRec :: ((a -> Next a, b -> Done b, a) -> Future e (Next a | Done b), a) -> Future e b

Implementation of Fantasy Land ChainRec. Since Fluture 6.0 introduced stack safety there should be no need to use this function directly. Instead it's recommended to use chain(rec).

Transforming Futures

map

map :: Functor m => (a -> b) -> m a -> m b
map                  :: Functor m  => (a -> b) -> m a -> m        b
Future.map           :: Functor m  => (a -> b) -> m a -> m        b
Par.map              :: Functor m  => (a -> b) -> m a -> m        b
Future.prototype.map :: Future e a ~> (a -> b)        -> Future e b

Transforms the resolution value inside the Future, and returns a Future with the new value. The transformation is only applied to the resolution branch: if the Future is rejected, the transformation is ignored.

See also chain and mapRej.

Future.of(1)
.map(x => x + 1)
.fork(console.error, console.log);
//> 2

For comparison, the equivalent with Promises is:

Promise.resolve(1)
.then(x => x + 1)
.then(console.log, console.error);

bimap

bimap :: Bifunctor m => (a -> c) -> (b -> d) -> m a b -> m c d
bimap                  :: Bifunctor m => (a -> c) -> (b -> d) -> m a b -> m      c d
Future.bimap           :: Bifunctor m => (a -> c) -> (b -> d) -> m a b -> m      c d
Future.prototype.bimap :: Future a b  ~> (a -> c,     b -> d)          -> Future c d

Maps the left function over the rejection value, or the right function over the resolution value, depending on which is present.

Future.of(1)
.bimap(x => x + '!', x => x + 1)
.fork(console.error, console.log);
//> 2

Future.reject('error')
.bimap(x => x + '!', x => x + 1)
.fork(console.error, console.log);
//! "error!"

For comparison, the equivalent with Promises is:

Promise.resolve(1)
.then(x => x + 1, x => x + '!')
.then(console.log, console.error);

chain

chain :: Chain m => (a -> m b) -> m a -> m b
chain                  :: Chain m    => (a -> m        b) -> m a -> m        b
Future.chain           :: Chain m    => (a -> m        b) -> m a -> m        b
Future.prototype.chain :: Future e a ~> (a -> Future e b) ->        Future e b

Sequence a new Future using the resolution value from another. Similarly to map, chain expects a function to transform the resolution value of a Future. But instead of returning the new value, chain expects a Future to be returned.

The transformation is only applied to the resolution branch: if the Future is rejected, the transformation is ignored.

See also chainRej.

Future.of(1)
.chain(x => Future.of(x + 1))
.fork(console.error, console.log);
//> 2

For comparison, the equivalent with Promises is:

Promise.resolve(1)
.then(x => Promise.resolve(x + 1))
.then(console.log, console.error);

swap

swap :: Future a b -> Future b a
swap                  :: Future a b -> Future b a
Future.prototype.swap :: Future a b ~> Future b a

Resolve with the rejection reason, or reject with the resolution value.

Future.of(new Error('It broke')).swap().fork(console.error, console.log);
//! [It broke]

Future.reject('Nothing broke').swap().fork(console.error, console.log);
//> "Nothing broke"

mapRej

mapRej :: (a -> c) -> Future a b -> Future c b
mapRej                  ::               (a -> c) -> Future a b -> Future c b
Future.prototype.mapRej :: Future a b ~> (a -> c)               -> Future c b

Map over the rejection reason of the Future. This is like map, but for the rejection branch.

Future.reject(new Error('It broke!'))
.mapRej(err => new Error('Oh No! ' + err.message))
.fork(console.error, console.log);
//! [Oh No! It broke!]

For comparison, the equivalent with Promises is:

Promise.resolve(1)
.then(null, err => Promise.reject(new Error('Oh No! ' + err.message)))
.then(console.log, console.error);

chainRej

chainRej :: (a -> Future c b) -> Future a b -> Future c b
chainRej                  ::               (a -> Future c b) -> Future a b -> Future c b
Future.prototype.chainRej :: Future a b ~> (a -> Future c b)               -> Future c b

Chain over the rejection reason of the Future. This is like chain, but for the rejection branch.

Future.reject(new Error('It broke!'))
.chainRej(err => Future.of(err.message + ' But it\'s all good.'))
.fork(console.error, console.log);
//> "It broke! But it's all good."

For comparison, the equivalent with Promises is:

Promise.reject(new Error('It broke!'))
.then(null, err => err.message + ' But it\'s all good.')
.then(console.log, console.error);

fold

fold :: (a -> c) -> (b -> c) -> Future a b -> Future d c
fold                  ::               (a -> c) -> (b -> c) -> Future a b -> Future d c
Future.prototype.fold :: Future a b ~> (a -> c,     b -> c)               -> Future d c

Applies the left function to the rejection value, or the right function to the resolution value, depending on which is present, and resolves with the result.

This provides a convenient means to ensure a Future is always resolved. It can be used with other type constructors, like S.Either, to maintain a representation of failure.

Future.of('hello')
.fold(S.Left, S.Right)
.value(console.log);
//> Right('hello')

Future.reject('it broke')
.fold(S.Left, S.Right)
.value(console.log);
//> Left('it broke')

For comparison, the equivalent with Promises is:

Promise.resolve('hello')
.then(S.Right, S.Left)
.then(console.log);

Combining Futures

ap

ap :: Apply m => m (a -> b) -> m a -> m b
ap                  :: Apply m => m        (a -> b) -> m        a -> m        b
Future.ap           :: Apply m => m        (a -> b) -> m        a -> m        b
Par.ap              :: Apply m => m        (a -> b) -> m        a -> m        b
Future.prototype.ap ::            Future e (a -> b) ~> Future e a -> Future e b

Applies the function contained in the left-hand Future or Apply to the value contained in the right-hand Future or Apply. If one of the Futures rejects the resulting Future will also be rejected.

Future.of(x => y => x + y)
.ap(Future.of(1))
.ap(Future.of(2))
.fork(console.error, console.log);
//> 3

and

and :: Future a b -> Future a c -> Future a c
and                  :: Future a b -> Future a c -> Future a c
Future.prototype.and :: Future a b ~> Future a c -> Future a c

Logical and for Futures.

Returns a new Future which either rejects with the first rejection reason, or resolves with the last resolution value once and if both Futures resolve. We can use it if we want a computation to run only after another has succeeded.

See also alt and finally.

Future.after(300, null)
.and(Future.of('hello'))
.fork(console.error, console.log);
//> "hello"

With good old reduce, we can turn this into an asynchronous all function, where the resulting Future will be the leftmost to reject, or the rightmost to resolve.

var all = ms => ms.reduce(Future.and, Future.of(0));
all([Future.after(20, 1), Future.of(2)]).value(console.log);
//> 2

alt

alt :: Alt f => f a -> f a -> f a
alt                  :: Alt f => f a -> f a -> f a
or                   :: Alt f => f a -> f a -> f a
Future.alt           :: Alt f => f a -> f a -> f a
Par.alt              :: Alt f => f a -> f a -> f a
Future.prototype.alt :: Future a b ~> Future a b -> Future a b
Future.prototype.or  :: Future a b ~> Future a b -> Future a b

Select one of two Alts.

Behaves like logical or on Future instances, returning a new Future which either resolves with the first resolution value, or rejects with the last rejection reason. We can use it if we want a computation to run only if another has failed.

Behaves like race on ConcurrentFuture instances.

This function has an alias or for legacy reasons.

See also and and finally.

Future.rejectAfter(300, new Error('Failed'))
.alt(Future.of('hello'))
.fork(console.error, console.log);
//> "hello"

With good old reduce, we can turn this into an asynchronous any function, where the resulting Future will be the leftmost to resolve, or the rightmost to reject.

var any = ms => ms.reduce(Future.alt, Future.reject('empty list'));
any([Future.reject(1), Future.after(20, 2), Future.of(3)]).value(console.log);
//> 2

finally

finally :: Future a c -> Future a b -> Future a b
finally                  ::               Future a c -> Future a b -> Future a b
lastly                   ::               Future a c -> Future a b -> Future a b
Future.prototype.finally :: Future a b ~> Future a c               -> Future a b
Future.prototype.lastly  :: Future a b ~> Future a c               -> Future a b

Run a second Future after the first settles (successfully or unsuccessfully). Rejects with the rejection reason from the first or second Future, or resolves with the resolution value from the first Future. We can use this when we want a computation to run after another settles, successfully or unsuccessfully.

If you're looking to clean up resources after running a computation which acquires them, you should use hook, which has many more fail-safes in place.

This function has an alias lastly, for environments where finally is reserved.

See also and and alt.

Future.of('Hello')
.finally(Future.of('All done!').map(console.log))
.fork(console.error, console.log);
//> "All done!"
//> "Hello"

Note that the first Future is given as the last argument to Future.finally():

var program = S.pipe([
  Future.of,
  Future.finally(Future.of('All done!').map(console.log)),
  Future.fork(console.error, console.log)
]);

program('Hello');
//> "All done!"
//> "Hello"

Consuming Futures

fork

fork :: (a -> Any) -> (b -> Any) -> Future a b -> Cancel
fork                  ::               (a -> Any) -> (b -> Any) -> Future a b -> Cancel
Future.prototype.fork :: Future a b ~> (a -> Any,     b -> Any)               -> Cancel

Execute the computation represented by a Future, passing reject and resolve callbacks to continue once there is a result.

This function is called fork because it literally represents a fork in our program: a point where a single code-path splits in two. It is recommended to keep the number of calls to fork at a minimum for this reason. The more forks, the higher the code complexity.

Generally, one only needs to call fork in a single place in the entire program.

After we fork a Future, the computation will start running. If the program decides halfway through that it's no longer interested in the result of the computation, it can call the unsubscribe function returned by fork(). See Cancellation.

Note that if an exception was encountered during the computation, it will be thrown and likely not be catchable. If the computation ran in isolation, we may want to use forkCatch instead to recover from exceptions.

Future.of('world').fork(
  err => console.log(`Oh no! ${err.message}`),
  thing => console.log(`Hello ${thing}!`)
);
//> "Hello world!"

Future.reject(new Error('It broke!')).fork(
  err => console.log(`Oh no! ${err.message}`),
  thing => console.log(`Hello ${thing}!`)
);
//! "Oh no! It broke!"

var consoleFork = Future.fork(console.error, console.log);
consoleFork(Future.of('Hello'));
//> "Hello"

forkCatch

forkCatch :: (Error -> Any) -> (a -> Any) -> (b -> Any) -> Future a b -> Cancel
forkCatch                  ::               (Error -> Any) -> (a -> Any) -> (b -> Any) -> Future a b -> Cancel
Future.prototype.forkCatch :: Future a b ~> (Error -> Any,     a -> Any,     b -> Any)               -> Cancel

An advanced version of fork that allows us to recover in the event that an error was thrown during the execution of the computation.

The recovery function will always be called with an instance of Error, independent of what was thrown.

Using this function is a trade-off;

Generally it's best to let a program crash and restart when an unexpected exception occurs. Restarting is the surest way to restore the memory that was allocated by the program to an expected state.

By using forkCatch, we might be able to keep our program alive longer, which can be very beneficial when the program is being used by multiple clients. However, we also forego the certainty that our program will be in a valid state after this happens. The more isolated the memory consumed by the particular computation was, the more certain we will be that recovery is safe.

See Debugging for information about the Error object that is recovered.

var fut = Future.after(300, null).map(x => x.foo);
fut.forkCatch(console.error, console.error, console.log);
//! Cannot read property 'foo' of null

value

value :: (b -> x) -> Future a b -> Cancel
value                  ::               (b -> x) -> Future a b -> Cancel
Future.prototype.value :: Future a b ~> (b -> x)               -> Cancel

Extracts the value from a resolved Future by forking it. Only use this function if you are sure the Future is going to be resolved, for example; after using fold. If the Future rejects and value was used, an uncatchable Error will be thrown.

Future.reject(new Error('It broke'))
.fold(S.Left, S.Right)
.value(console.log);
//> Left([Error: It broke])

As with fork, value returns an unsubscribe function. See Cancellation.

done

done :: Nodeback a b -> Future a b -> Cancel
done                  ::               Nodeback a b -> Future a b -> Cancel
Future.prototype.done :: Future a b ~> Nodeback a b               -> Cancel

Fork the Future into a Nodeback.

This is like fork, but instead of taking two unary functions, it takes a single binary function.

As with fork, done returns an unsubscribe function. See Cancellation.

Future.of('hello').done((err, val) => console.log(val));
//> "hello"

promise

promise :: Future a b -> Promise b a
promise                  :: Future a b -> Promise b a
Future.prototype.promise :: Future a b ~> Promise b a

An alternative way to fork the Future. Returns a Promise which resolves with the resolution value, or rejects with the rejection reason of the Future.

Note that if an exception was encountered during the computation, it will be thrown and likely not be catchable.

Future.of('Hello').promise().then(console.log);
//> "Hello"

This is a convenience function which provides a "quick and dirty" way to create a Promise from a Future. You should only use it in scenarios where you're not interested in cancellation, nor interested in recovering from exceptions. For example in a test runner that wants you to give it a Promise. In any other scenario, if you really want a Promise, you should probably make a custom wrapper around forkCatch to create your Promise, for example:

const eventualThing = Future.after(300, 'World');

new Promise((res, rej) => {

  // We've decided that an exception should go to the rejection branch, and
  // we're wrapping the failure or success values to not lose information.
  const cancel = eventualThing.forkCatch(rej, reason => {
    res({success: false, reason: reason, value: null});
  }, value => {
    res({success: true, reason: null, value: value});
  });

  // We're also handling cancellation here.
  process.on('SIGINT', cancel);

});

Parallelism

race

race :: Future a b -> Future a b -> Future a b
race                  :: Future a b -> Future a b -> Future a b
Future.prototype.race :: Future a b ~> Future a b -> Future a b

Race two Futures against each other. Creates a new Future which resolves or rejects with the resolution or rejection value of the first Future to settle.

When one Future settles, the other gets cancelled automatically.

Future.after(100, 'hello')
.race(Future.after(50, 'bye'))
.fork(console.error, console.log);
//> "bye"

With good old reduce, we can turn this into a first function, where the resulting Future will be the first to resolve, or the first to reject.

var first = futures => futures.reduce(Future.race, Future.never);
first([
  Future.after(100, 'hello'),
  Future.after(50, 'bye'),
  Future.rejectAfter(25, 'nope')
])
.fork(console.error, console.log);
//! "nope"

both

both :: Future a b -> Future a c -> Future a (Pair b c)
both                  :: Future a b -> Future a c -> Future a (Pair b c)
Future.prototype.both :: Future a b ~> Future a c -> Future a (Pair b c)

Run two Futures in parallel and get a Pair of the results. When either Future rejects, the other Future will be cancelled and the resulting Future will reject.

var a = Future.of('a');
var b = Future.of('b');

Future.both(a, b).fork(console.error, console.log);
//> ['a', 'b']

parallel

parallel :: PositiveInteger -> Array (Future a b) -> Future a (Array b)
parallel :: PositiveInteger -> Array (Future a b) -> Future a (Array b)

Creates a Future which when forked runs all Futures in the given Array in parallel, ensuring no more than limit Futures are running at once.

When one Future rejects, all currently running Futures will be cancelled and the resulting Future will reject.

var tenFutures = Array.from(Array(10).keys()).map(Future.after(20));

//Runs all Futures in sequence:
Future.parallel(1, tenFutures).fork(console.error, console.log);
//after about 200ms:
//> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

//Runs upto five Futures in parallel:
Future.parallel(5, tenFutures).fork(console.error, console.log);
//after about 40ms:
//> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

//Runs all Futures in parallel:
Future.parallel(Infinity, tenFutures).fork(console.error, console.log);
//after about 20ms:
//> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

If you want to settle all Futures, even if some may fail, you can use this in combination with fold:

var unstableFutures = Array.from({length: 4}, (_, i) =>
  Future.node(done => done(Math.random() > 0.75 ? 'failed' : null, i))
);

var stabalizedFutures = unstableFutures.map(Future.fold(S.Left, S.Right));

Future.parallel(Infinity, stabalizedFutures).fork(console.error, console.log);
//> [ Right(0), Left("failed"), Right(2), Right(3) ]

ConcurrentFuture

The ConcurrentFuture type is the result of applying concurrify to Future. It provides a mechanism for constructing a Fantasy Land Alternative from a member of Future. This allows Futures to benefit from the Alternative Interface, which includes parallel ap, zero and alt.

The idea is that we can switch back and forth between Future and ConcurrentFuture, using Par and seq, to get sequential or concurrent behaviour respectively. It's a useful type to pass to abstractions that don't know about Future-specific functions like parallel or race, but do know how to operate on Apply and Alternative.

var {of, ap, sequence} = require('sanctuary');
var {Future, Par, seq} = require('fluture');

//Some dummy values
var x = 1;
var f = a => a + 1;

//The following two are equal ways to construct a ConcurrentFuture
var parx = of(Par, x);
var parf = Par(of(Future, f));

//We can make use of parallel apply
seq(ap(parx, parf)).value(console.log);
//> 2

//Or concurrent sequencing
seq(sequence(Par, [parx, parf])).value(console.log);
//> [x, f]
Par

Converts a Future to a ConcurrentFuture.

Par :: Future a b -> ConcurrentFuture a b
Par :: Future a b -> ConcurrentFuture a b
Par.of

Constructs a ConcurrentFuture with the given resolution value.

Par.of :: b -> ConcurrentFuture a b
Par.of :: b -> ConcurrentFuture a b
Par.zero

Constructs a ConcurrentFuture which will never resolve or reject with anything.

Par.zero :: () -> ConcurrentFuture a a
Par.zero :: () -> ConcurrentFuture a a
seq

Converts a ConcurrentFuture to a Future.

seq :: ConcurrentFuture a b -> Future a b
seq :: ConcurrentFuture a b -> Future a b

Resource management

Functions listed under this category allow for more fine-grained control over the flow of acquired values.

hook

hook :: Future a b -> (b -> Future a c) -> (b -> Future a d) -> Future a d
hook :: Future a b -> (b -> Future a c) -> (b -> Future a d) -> Future a d

Allows a Future-returning function to be decorated with resource acquisition and disposal. The signature is like hook(acquire, dispose, consume), where acquire is a Future which might create connections, open file handlers, etc. dispose is a function that takes the result from acquire and should be used to clean up (close connections etc) and consume also takes the result from acquire, and may be used to perform any arbitrary computations using the resource. The resolution value of dispose is ignored.

var withConnection = Future.hook(
  openConnection('localhost'),
  closeConnection
);

withConnection(
  conn => query(conn, 'EAT * cakes FROM bakery')
)
.fork(console.error, console.log);

When a hooked Future is cancelled while acquiring its resource, nothing else will happen. However, once the resource has been acquired, the dispose Future is guaranteed to run.

In the case where the consumption Future settles, the disposal is forked and awaited. If the disposal Future rejects or encounters an exception, it will replace the state of the consumption Future. Otherwise the rejection reason or resolution value of the consumption Future will determine the final outcome.

In the case where an exception is encountered during consumption, the disposal Future will run, but its results are ignored.

In the case where the consumption or disposal Future is cancelled from outside, the disposal Future will start and/or finish its computation, but the results will be ignored.

Utility functions

pipe

pipe :: Future a b ~> (Future a b -> c) -> c
Future.prototype.pipe :: Future a b ~> (Future a b -> c) -> c

A method available on all Futures to allow arbitrary functions over Futures to be included in a fluent-style method chain.

This method is particularly useful in combination with functions derived from Fantasy Land implementations, for example S.join:

Future.of(42)
.map(Future.resolve)
.pipe(S.join)
.value(console.log);
//> 42

cache

cache :: Future a b -> Future a b
cache :: Future a b -> Future a b

Returns a Future which caches the resolution value or rejection reason of the given Future so that whenever it's forked, it can load the value from cache rather than re-executing the underlying computation.

This essentially turns a unicast Future into a multicast Future, allowing multiple consumers to subscribe to the same result. The underlying computation is never cancelled unless all consumers unsubscribe before it completes.

There is a glaring drawback to using cache, which is that returned Futures are no longer referentially transparent, making reasoning about them more difficult and refactoring code that uses them harder.

var {readFile} = require('fs');
var eventualPackage = Future.cache(
  Future.node(done => {
    console.log('Reading some big data');
    readFile('package.json', 'utf8', done);
  })
);

eventualPackage.fork(console.error, console.log);
//> "Reading some big data"
//> "{...}"

eventualPackage.fork(console.error, console.log);
//> "{...}"

isFuture

isFuture :: a -> Boolean
isFuture :: a -> Boolean

Returns true for Futures and false for everything else. This function (and S.is) also return true for instances of Future that were created within other contexts. It is therefore recommended to use this over instanceof, unless your intent is to explicitly check for Futures created using the exact Future constructor you're testing against.

var Future1 = require('/path/to/fluture');
var Future2 = require('/other/path/to/fluture');
var noop = () => {};

var m1 = Future1(noop);
Future1.isFuture(m1) === (m1 instanceof Future1);
//> true

var m2 = Future2(noop);
Future1.isFuture(m2) === (m2 instanceof Future1);
//> false

never

never :: Future a a
never :: Future a a

A Future that never settles. Can be useful as an initial value when reducing with race, for example.

isNever

isNever :: a -> Boolean
isNever :: a -> Boolean

Returns true if the given input is a never.

extractLeft

extractLeft :: Future a b -> Array a
extractLeft                  :: Future a b       -> Array a
Future.prototype.extractLeft :: Future a b ~> () -> Array a

Returns an array whose only element is the rejection reason of the Future. In many cases it will be impossible to extract this value; In those cases, the array will be empty. This function is meant to be used for type introspection: it is not the correct way to consume a Future.

extractRight

extractRight :: Future a b -> Array b
extractRight                  :: Future a b       -> Array b
Future.prototype.extractRight :: Future a b ~> () -> Array b

Returns an array whose only element is the resolution value of the Future. In many cases it will be impossible to extract this value; In those cases, the array will be empty. This function is meant to be used for type introspection: it is not the correct way to consume a Future.

debugMode

debugMode :: Boolean -> Undefined
debugMode :: Boolean -> Undefined

Enable or disable Fluture's debug mode. Debug mode is disabled by default. Pass true to enable, or false to disable.

Future.debugMode(true);

For more information, see Debugging and Context.

context

context :: Future a b ~> List Context

A linked list of debugging contexts made available on every instance of Future. When debug mode is disabled, the list is always empty.

The context objects have stack properties which contain snapshots of the stacktraces leading up to the creation of the Future instance. They are used by Fluture to generate asynchronous stack traces.

Future.debugMode(true);
const future = Future.after(10, 'Hello');

let context = future.context;
while(context.head){
  console.log(context.head.stack);
  context = context.tail;
}

License

MIT licensed