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
In ES5 or older environments
Fluture depends on these functions being present:
Object.create
,
Object.assign
and Array.isArray
.
You may need to polyfill one or more.
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);
In ES6 or newer environments
The package.json
sets a module
-field for build-tools like Rollup.
import {readFile} from 'fs';
import {node, encase} from 'fluture';
const getPackageName = file =>
node(done => { readFile(file, 'utf8', done) })
.chain(encase(JSON.parse))
.map(x => x.name);
getPackageName('package.json')
.fork(console.error, console.log);
Interoperability
Future
implements Fantasy Land and Static Land -compatible
Bifunctor
, Monad
and ChainRec
(of
, ap
, map
, bimap
, chain
, chainRec
).
All versions of Fantasy Land are supported.Future.Par
implements Fantasy Land 3 Alternative
(of
, zero
, map
, ap
, alt
).- The Future representative contains a
@@type
property for Sanctuary Type Identifiers.
Butterfly
The name "Fluture" is a conjunction of "FL" (the acronym to Fantasy Land)
and "future". Fluture means butterfly in Romanian: A creature you might expect
to see in Fantasy Land.
Thanks to Erik Fuente for 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 and utilities
Type signatures
Hindley-Milner type signatures are used to document functions, with
one addition: In order to document methods as well as static functions, we
prefix every signature with .
or #
. This indicates whether the function
lives on the object (.function
), or its prototype (#method
). We use ~>
to
separate the implicit this
argument from the other, explicit, arguments.
Types
You'll find that some signatures refer to concrete types, such as Future
.
This is reference of the types used throughout the documentation:
Type classes
Some signatures contain "constrained type variables". These constraints are
expressed by means of type classes, specifically those defined by
Fantasy Land and verified by Sanctuary Type Classes:
Stack safety
Fluture interprets your transformations in a stack safe way. This means that
none of the following operations raise RangeError: Maximum call stack size exceeded
:
const add1 = x => x + 1;
let m = Future.of(1);
for(let i = 0; i < 100000; i++){
m = m.map(add1);
}
m.fork(console.error, console.log);
const m = (function recur(x){
const mx = Future.of(x + 1);
return x < 100000 ? mx.chain(recur) : mx;
}(1));
m.fork(console.error, console.log);
To learn more about memory and stack usage under different types of recursion,
see (or execute) scripts/test-mem
.
Sanctuary
When using this module with Sanctuary Def (and Sanctuary by
extension) you might run into the following issue:
const S = require('sanctuary');
const Future = require('fluture');
S.I(Future.of(1));
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
:
const {create, env} = require('sanctuary');
const {env: flutureEnv} = require('fluture-sanctuary-types');
const Future = require('fluture');
const S = create({checkTypes: true, env: env.concat(flutureEnv)});
S.I(Future.of(1));
Casting Futures
Sometimes you 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:
const NoFuture = require('incompatible-future');
const incompatible = NoFuture.of('Hello');
const compatible = Future(incompatible.fork.bind(incompatible));
compatible.both(Future.of('world')).value(console.log);
Creating Futures
Future
Future :: ((a -> (), b -> ()) -> 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 resolve
, which abbreviates
to res
. When the computation is finished (possibly asynchronously) it may call
the appropriate continuation with a failure or success value.
Future(function computation(reject, resolve){
const x = setTimeout(resolve, 3000, 'world');
return () => clearTimeout(x);
});
Additionally, the computation may return a nullary function containing
cancellation logic. This function is executed when the Future is cancelled
after it's forked.
of
.of :: a -> Future _ a
Creates a Future which immediately resolves with the given value. This function
is compliant with the Fantasy Land Applicative specification.
const eventualThing = Future.of('world');
eventualThing.fork(
console.error,
thing => console.log(`Hello ${thing}!`)
);
reject
.reject :: a -> Future a _
Creates a Future which immediately rejects with the given value. Just like of
but for the rejection branch.
after
.after :: Number -> b -> Future a b
Creates a Future which resolves with the given value after n milliseconds.
const eventualThing = Future.after(500, 'world');
eventualThing.fork(console.error, thing => console.log(`Hello ${thing}!`));
rejectAfter
.rejectAfter :: Number -> a -> Future a b
Creates a Future which rejects with the given reason after n milliseconds.
const eventualError = Future.rejectAfter(500, new Error('Kaputt!'));
eventualError.fork(err => console.log('Oh no - ' + err.message), console.log);
do
.do :: (() -> Iterator) -> Future a b
.go :: (() -> Iterator) -> Future a b
A specialized version of fantasy-do which works only for Futures, but has
the advantage of type-checking and not having to pass Future.of
. Another
advantage is that the returned Future can be forked multiple times, as opposed
to with a general fantasy-do
solution, where forking the Future a second time
behaves erroneously.
Takes a function which returns an Iterator, commonly a
generator-function, and chains every produced Future over the previous.
This allows for writing sequential asynchronous code without the pyramid of
doom. It's known as "coroutines" in Promise land, and "do-notation" in Haskell
land.
Future.do(function*(){
const thing = yield Future.after(300, 'world');
const message = yield Future.after(300, 'Hello ' + thing);
return message + '!';
})
.fork(console.error, console.log);
Error handling is slightly different in do-notation, you need to fold
the error into your control domain, I recommend folding into an Either
:
const attempt = Future.fold(S.Left, S.Right);
const ajaxGet = url => Future.reject('Failed to load ' + url);
Future.do(function*(){
const e = yield attempt(ajaxGet('/message'));
return S.either(
e => `Oh no! ${e}`,
x => `Yippee! ${x}`,
e
);
})
.fork(console.error, console.log);
This function has an alias go
, for environments in which do
is a reserved word.
try
.try :: (() -> !a | b) -> Future a b
.attempt :: (() -> !a | b) -> Future a b
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)
.
const data = {foo: 'bar'};
Future.try(() => data.foo.bar.baz)
.fork(console.error, console.log);
This function has an alias attempt
, for environments in which try
is a reserved word.
tryP
.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);
node
.node :: (Nodeback e r -> ()) -> 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.
This is very similar to the Future()
-constructor, except that it
takes a single function with two arguments instead of two functions with a
single argument.
Short for Future.encaseN(f, undefined)
.
Future.node(done => {
done(null, 'Hello');
})
.fork(console.error, console.log);
encase
.encase :: (a -> !e | r) -> a -> Future e r
.encase2 :: (a, b -> !e | r) -> a -> b -> Future e r
.encase3 :: (a, b, c -> !e | 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:
const data = '{"foo" = "bar"}';
Future.encase(JSON.parse, data)
.fork(console.error, console.log);
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):
const data = '{"foo" = "bar"}';
const safeJsonParse = Future.encase(JSON.parse);
safeJsonParse(data).fork(console.error, console.log);
Furthermore; encase2
and encase3
are binary and ternary versions of
encase
, applying two or three arguments to the given function respectively.
encaseP
.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.
const 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);
Furthermore; encaseP2
and encaseP3
are binary and ternary versions
of encaseP
, applying two or three arguments to the given function respectively.
encaseN
.encaseN :: ((a, Nodeback e r) -> ()) -> a -> Future e r
.encaseN2 :: ((a, b, Nodeback e r) -> ()) -> a -> b -> Future e r
.encaseN3 :: ((a, b, c, Nodeback e r) -> ()) -> 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.
const fs = require('fs');
const read = Future.encaseN2(fs.readFile);
read('README.md', 'utf8')
.map(text => text.split('\n'))
.map(lines => lines[0])
.fork(console.error, console.log);
Furthermore; encaseN2
and encaseN3
are binary and ternary versions
of encaseN
, applying two or three arguments to the given function respectively.
chainRec
.chainRec :: ((a -> c, b -> c, a) -> Future e c, 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 :: Future a b ~> (b -> c) -> Future a c
.map :: Functor m => (a -> b) -> m a -> m b
Transforms the resolution value inside the Future, and returns a new Future with
the transformed value. This is like doing promise.then(x => x + 1)
, except
that it's lazy, so the transformation will not be applied before the Future is
forked. The transformation is only applied to the resolution branch: If the
Future is rejected, the transformation is ignored. To learn more about the exact
behaviour of map
, check out its spec.
Future.of(1)
.map(x => x + 1)
.fork(console.error, console.log);
bimap
#bimap :: Future a b ~> (a -> c) -> (b -> d) -> Future c d
.bimap :: Bifunctor m => (a -> b) -> (c -> d) -> m a c -> m b 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);
Future.reject('error')
.bimap(x => x + '!', x => x + 1)
.fork(console.error, console.log);
chain
#chain :: Future a b ~> (b -> Future a c) -> Future a c
.chain :: Chain m => (a -> m b) -> m a -> m b
Allows the creation of a new Future based on the resolution value. This is like
doing promise.then(x => Promise.resolve(x + 1))
, except that it's lazy, so the
new Future will not be created until the other one is forked. The function is
only ever applied to the resolution value; it's ignored when the Future was
rejected. To learn more about the exact behaviour of chain
, check out its spec.
Future.of(1)
.chain(x => Future.of(x + 1))
.fork(console.error, console.log);
swap
#swap :: Future a b ~> Future b a
.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);
Future.reject('Nothing broke').swap().fork(console.error, console.log);
mapRej
#mapRej :: Future a b ~> (a -> c) -> Future c b
.mapRej :: (a -> b) -> Future a c -> Future b c
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('Some extra info: ' + err.message))
.fork(console.error, console.log);
chainRej
#chainRej :: Future a b ~> (a -> Future a c) -> Future a c
.chainRej :: (a -> Future a c) -> Future a b -> Future a c
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 => {
console.error(err);
return Future.of('All is good');
})
.fork(console.error, console.log);
fold
#fold :: Future a b ~> (a -> c, b -> c) -> Future _ c
.fold :: (a -> c) -> (b -> c) -> Future a b -> Future _ 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 failures:
Future.of('hello')
.fold(S.Left, S.Right)
.value(console.log);
Future.reject('it broke')
.fold(S.Left, S.Right)
.value(console.log);
Combining Futures
ap
#ap :: Future a (b -> c) ~> Future a b -> Future a c
.ap :: Apply m => m (a -> b) -> m a -> m 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);
Note that even though #ap
does not conform to the latest spec,
the hidden fantasy-land/ap
-method does. Therefore Future remains fully
compliant to Fantasy Land.
and
#and :: Future a b ~> Future a b -> Future a b
.and :: Future a b -> Future a b -> Future a b
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. This
behaves analogously to how JavaScript's and-operator does.
isResolved().and(getValue());
const all = ms => ms.reduce(Future.and, Future.of(true));
all([Future.after(20, 1), Future.of(2)]).value(console.log);
or
#or :: Future a b ~> Future a b -> Future a b
.or :: Future a b -> Future a b -> Future a b
Logical or for Futures.
Returns a new Future which either resolves with the first resolution value, or
rejects with the last rejection value once and if both Futures reject. This
behaves analogously to how JavaScript's or-operator does.
planA().or(planB());
const any = ms => ms.reduce(Future.or, Future.reject('empty list'));
any([Future.reject(1), Future.after(20, 2), Future.of(3)]).value(console.log);
Consuming Futures
fork
#fork :: Future a b ~> (a -> (), b -> ()) -> Cancel
.fork :: (a -> ()) -> (b -> ()) -> Future a b -> Cancel
Execute the computation that was passed to the Future at construction
using the given reject
and resolve
callbacks.
Future.of('world').fork(
err => console.log(`Oh no! ${err.message}`),
thing => console.log(`Hello ${thing}!`)
);
Future.reject(new Error('It broke!')).fork(
err => console.log(`Oh no! ${err.message}`),
thing => console.log(`Hello ${thing}!`)
);
const consoleFork = Future.fork(console.error, console.log);
consoleFork(Future.of('Hello'));
After you fork
a Future, the computation will start running. If you wish to
cancel the computation, you may use the function returned by fork
:
const fut = Future.after(300, 'hello');
const cancel = fut.fork(console.error, console.log);
cancel();
value
#value :: Future a b ~> (b -> ()) -> Cancel
.value :: (b -> ()) -> Future a b -> 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 (likely uncatchable)
Error
will be thrown.
Future.reject(new Error('It broke'))
.fold(S.Left, S.Right)
.value(console.log);
Just like fork, value
returns the Cancel
function:
Future.after(300, 'hello').value(console.log)();
done
#done :: Future a b ~> Nodeback a b -> Cancel
.done :: Nodeback a b -> Future a b -> Cancel
Fork the Future into a Nodeback.
Future.of('hello').done((err, val) => console.log(val));
This is like fork, but instead of taking two unary functions, it takes
a single binary function. As with fork()
, done()
returns Cancel
:
const m = Future.after(300, 'hello');
const cancel = m.done((err, val) => console.log(val));
cancel();
promise
#promise :: Future a b ~> Promise b a
.promise :: Future a b -> Promise b a
An alternative way to fork
the Future. This eagerly forks the Future and
returns a Promise of the result. This is useful if some API wants you to give it
a Promise. It's the only method which forks the Future without a forced way to
handle the rejection branch, so I recommend against using it for anything else.
Future.of('Hello').promise().then(console.log);
Parallelism
race
#race :: Future a b ~> Future a b -> Future a b
.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.
Future.after(100, 'hello')
.race(Future.after(50, 'bye'))
.fork(console.error, console.log);
const 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);
both
#both :: Future a b ~> Future a b -> Future a b
.both :: Future a b -> Future a b -> Future a b
Run two Futures in parallel. Basically like calling
Future.parallel
with exactly two Futures:
const a = Future.of('a');
const b = Future.of('b');
Future.both(a, b).fork(console.error, console.log);
parallel
.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.
const tenFutures = Array.from(Array(10).keys()).map(Future.after(20));
Future.parallel(1, tenFutures).fork(console.error, console.log);
Future.parallel(5, tenFutures).fork(console.error, console.log);
Future.parallel(Infinity, tenFutures).fork(console.error, console.log);
If you want to settle all Futures, even if some may fail, you can use this in
combination with fold:
const instableFutures = Array.from({length: 4}, (_, i) =>
Future.node(done => done(Math.random() > 0.75 ? 'failed' : null, i))
);
const stabalizedFutures = instableFutures.map(Future.fold(S.Left, S.Right));
Future.parallel(Infinity, stabalizedFutures).fork(console.error, console.log);
ConcurrentFuture
.Par :: Future a b -> ConcurrentFuture a b
.seq :: ConcurrentFuture a b -> Future a b
ConcurrentFuture (or Par
for short) 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 you can switch back and forth between Future
and
ConcurrentFuture
, using Par
and seq
, to get sequential or concurrent
behaviour respectively. It's useful if you want a purely algebraic alternative
to parallel
and race
.
const {of, ap, zero, alt, sequence} = require('sanctuary');
const {Future, Par, seq} = require('fluture');
const x = 1;
const f = a => a + 1;
const parx = of(Par, x);
const parf = Par(of(Future, f));
seq(ap(parx, parf)).value(console.log);
seq(sequence(Par, [parx, parf])).value(console.log);
seq(alt(zero(Par), parx)).value(console.log);
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
Allows a Future-returning function to be decorated with resource acquistion
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.
const withConnection = Future.hook(
openConnection('localhost'),
closeConnection
);
withConnection(
conn => query(conn, 'EAT * cakes FROM bakery')
)
.fork(console.error, console.log);
In the case that a hooked Future is cancelled after the resource was acquired,
dispose
will be executed and immediately cancelled. This means that rejections
which may happen during this disposal are silently ignored. To ensure that
resources are disposed during cancellation, you might synchronously dispose
resources in the cancel
function of the disposal Future:
const closeConnection = conn => Future((rej, res) => {
conn.flushGracefully(err => {
if(err === null){
conn.close();
res();
}else{
rej(err);
}
});
return () => conn.close();
});
finally
#finally :: Future a b ~> Future a c -> Future a b
#lastly :: Future a b ~> Future a c -> Future a b
.finally :: Future a c -> Future a b -> Future a b
.lastly :: Future a c -> Future a b -> 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.
Future.of('Hello')
.finally(Future.of('All done!').map(console.log))
.fork(console.error, console.log);
Note that the first Future is given as the last argument to Future.finally()
:
const program = S.pipe([
Future.of,
Future.finally(Future.of('All done!').map(console.log)),
Future.fork(console.error, console.log)
]);
program('Hello');
As with hook
; when the Future is cancelled before the finally
computation is running, the finally computation is executed and immediately
cancelled.
This function has an alias lastly
, for environments in which finally
is a reserved word.
Utility functions
cache
.cache :: Future a b -> Future a b
Returns a Future which caches the resolution value of the given Future so that
whenever it's forked, it can load the value from cache rather than reexecuting
the chain.
const {readFile} = require('fs');
const eventualPackage = Future.cache(
Future.node(done => {
console.log('Reading some big data');
readFile('package.json', 'utf8', done);
})
);
eventualPackage.fork(console.error, console.log);
eventualPackage.fork(console.error, console.log);
isFuture
.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.
const Future1 = require('/path/to/fluture');
const Future2 = require('/other/path/to/fluture');
const noop = () => {};
const m1 = Future1(noop);
Future1.isFuture(m1) === (m1 instanceof Future1);
const m2 = Future2(noop);
Future1.isFuture(m2) === (m2 instanceof Future1);
never
.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
Returns true
if the given input is a never
.
License
MIT licensed