- Notifications
You must be signed in to change notification settings - Fork84
🦋 Fantasy Land compliant (monadic) alternative to Promises
License
fluture-js/Fluture
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Fluture offers a control structure similar to Promises, Tasks, Deferreds, andwhat-have-you. Let's call them Futures.
Much like Promises, Futures represent the value arising from the success orfailure of an asynchronous operation (I/O). Though unlike Promises, Futures arelazy and adhere tothemonadic interface.
Some of the features provided by Fluture include:
- Cancellation.
- Resource management utilities.
- Stack safe composition and recursion.
- Integration withSanctuary.
- A pleasant debugging experience.
For more information:
- API documentation
- Article: Introduction to Fluture - A Functional Alternative to Promises
- Wiki: Compare Futures to Promises
- Wiki: Compare Fluture to similar libraries
- Video: Monad a Day - Futures by @DrBoolean
$npm install --save fluture
To load Fluture directly into a browser, a code pen, orDeno, use one ofthe following downloads from the JSDelivr content delivery network. These aresingle files that come with all of Fluture's dependencies pre-bundled.
- Fluture Script: A JavaScript file that adds
Fluture
to the globalscope. Ideal for older browsers and code pens. - Fluture Script Minified: The same as above, but minified.
- Fluture Module: An EcmaScript module with named exports. Ideal for Denoor modern browsers.
- Fluture Module Minified: A minified EcmaScript module without TypeScripttypings. Not recommended for Deno.
Fluture is written as modular JavaScript.
- On Node 12 and up, Fluture can be loaded directly with
import 'fluture'
. - On some older (minor) Node versions, you may need to import from
'fluture/index.js'
instead, and/or pass--experimental-modules
tonode
. - On Node versions below 12, theesm loader can be used. Alternatively,there is aCommonJS Module available.
- Modern browsers can run Fluture directly. If you'd like to try this out,I recommend installing Fluture withPika orSnowpack. You can alsotry thebundled module to avoid a package manager.
- For older browsers, use a bundler such asRollup or WebPack. Besides themodule system, Fluture uses purely ES5-compatible syntax, so the source doesnot have to be transpiled after bundling. Alternatively, there is aCommonJS Module available.
import{readFile}from'fs'import{node,encase,chain,map,fork}from'fluture'constgetPackageName=file=>(node(done=>{readFile(file,'utf8',done)}).pipe(chain(encase(JSON.parse))).pipe(map(x=>x.name)))getPackageName('package.json').pipe(fork(console.error)(console.log))
Although the Fluture source uses the EcmaScript module system,themain
file points to a CommonJS version of Fluture.
On older environments one or more of the following functions may need to bepolyfilled:Object.create
,Object.assign
andArray.isArray
.
constfs=require('fs')constFuture=require('fluture')constgetPackageName=function(file){returnFuture.node(function(done){fs.readFile(file,'utf8',done)}).pipe(Future.chain(Future.encase(JSON.parse))).pipe(Future.map(function(x){returnx.name}))}getPackageName('package.json').pipe(Future.fork(console.error)(console.log))
General
Creating new Futures
Future
: Create a possibly cancellable Futureresolve
: Create a resolved Futurereject
: Create a rejected Futureafter
: Create a Future that resolves after a timeoutrejectAfter
: Create a Future that rejects after a timeoutgo
: Create a "coroutine" using a generator functionattempt
: Create a Future using a possibly throwing functionattemptP
: Create a Future using a Promise-returning functionnode
: Create a Future using a Node-style callbackencase
: Convert a possibly throwing function to a Future functionencaseP
: Convert a Promise-returning function to a Future function
Converting between Nodeback APIs and Futures
Converting between Promises and Futures
Transforming and combining Futures
pipe
: Apply a function to a Future in a fluent method chainmap
: Synchronously process the success value in a Futurebimap
: Synchronously process the success or failure value in a Futurechain
: Asynchronously process the success value in a Futurebichain
: Asynchronously process the success or failure value in a Futureswap
: Swap the success with the failure valuemapRej
: Synchronously process the failure value in a FuturechainRej
: Asynchronously process the failure value in a Futurecoalesce
: Coerce success and failure values into the same success valueap
: Combine the success values of multiple Futures using a functionpap
: Combine the success values of multiple Futures in parallel using a functionand
: Logicaland for Futuresalt
: Logicalor for Futureslastly
: Run a Future after the previous settlesrace
: Race two Futures against each otherboth
: Await both success values from two Futuresparallel
: Await all success values from many Futures
Consuming/forking Futures
Concurrency related utilities and data structures
pap
: Combine the success values of multiple Futures in parallel using a functionrace
: Race two Futures against each otherboth
: Await both success values from two Futuresparallel
: Await all success values from many FuturesConcurrentFuture
: A separate data-type for doing algebraic concurrencyalt
: Behaves likerace
onConcurrentFuture
instances
Resource management
Other utilities
pipe
: Apply a function to a Future in a fluent method chaincache
: Cache a Future so that it can be forked multiple timesisFuture
: Determine whether a value is a Fluture compatible Futurenever
: A Future that never settlesdebugMode
: Configure Fluture's debug modecontext
: The debugging context of a Future instance
The name "Fluture" is a conjunction of "FL" (the acronym toFantasy Land)and "future". Fluture means butterfly in Romanian: A creature one might expectto see in Fantasy Land.
Credit goes to Erik Fuente for styling the logo, andWEAREREASONABLEPEOPLEfor sponsoring the project.
Future
implementsFantasy Land 1.0+ -compatibleAlt
,Bifunctor
,Monad
, andChainRec
(of
,ap
,alt
,map
,bimap
,chain
,chainRec
).Future.Par
implementsFantasy Land 3 -compatibleAlternative
(of
,zero
,map
,ap
,alt
).- The Future and ConcurrentFuture representatives contain
@@type
propertiesforSanctuary Type Identifiers. - The Future and ConcurrentFuture instances contain
@@show
properties forSanctuary Show.
The various function signatures are provided in a small language referred to asHindley-Milner notation.
In summary, the syntax is as follows:InputType -> OutputType
. Now,because functions in Fluture arecurried, the "output" of afunction is oftenanother function. In Hindley-Milner that's simply writtenasInputType -> InputToSecondFunction -> OutputType
and so forth.
By convention, types starting with an upper-case letter areconcrete types. When they start with a lower-case letter they'retype variables. You can think of these type variables as generic types.Soa -> b
denotes a function from generic typea
to generic typeb
.
Finally, through so-calledconstraints, type variables canbe forced to conform to an "interface" (orType Class in functional jargon).For example,MyInterface a => a -> b
, denotes a function from generic typea
to generic typeb
,wherea
must implementMyInterface
.
You can read in depth aboutHindley-Milner in JavaScript here.
The concrete types you will encounter throughout this documentation:
- Future - Instances of Future provided bycompatible versions of Fluture.
- ConcurrentFuture - Futures wrapped with (
Future.Par
). - Promise a b - Values which conform to thePromises/A+ specificationand have a rejection reason of type
a
and a resolution value of typeb
. - 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 theIterator protocol. - Cancel - The nullarycancellation functions returned from computations.
- Throwing e a b - A function from
a
tob
that may throw an exceptione
. - List - Fluture's internal linked-list structure:
{ head :: Any, tail :: List }
. - Context - Fluture's internal debugging context object:
{ tag :: String, name :: String, stack :: String }
.
Some signatures containconstrained type variables.Generally, these constraints express that some value must conform to aFantasy Land-specified interface.
- Functor -Fantasy Land Functor conformant values.
- Bifunctor -Fantasy Land Bifunctor conformant values.
- Chain -Fantasy Land Chain conformant values.
- Apply -Fantasy Land Apply conformant values.
- Alt -Fantasy Land Alt conformant values.
Cancellation is a system whereby running Futures get an opportunity to stopwhat they're doing and release resources that they were holding, when theconsumer indicates it is no longer interested in the result.
To cancel a Future, it must be unsubscribed from. Most of theconsumption functions return anunsubscribe
function.Calling it signals that we are no longer interested in the result. Aftercallingunsubscribe
, Fluture guarantees that our callbacks will not becalled; but more importantly: a cancellation signal is sent upstream.
The cancellation signal travels all the way back to the source (with theexception of cached Futures - seecache
), allowing all partiesalong the way to clean up.
With theFuture
constructor, we can provide a custom cancellationhandler by returning it from the computation. Let's see what this looks like:
// We use the Future constructor to create a Future instance.consteventualAnswer=Future(functioncomputeTheAnswer(rej,res){// We give the computer time to think about the answer, which is 42.consttimeoutId=setTimeout(res,60000,42)// Here is how we handle cancellation. This signal is received when nobody// is interested in the answer any more.returnfunctiononCancel(){// 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.constunsubscribe=fork(log('rejection'))(log('resolution'))(eventualAnswer)// 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: callingclearTimeout
.
Finally, Fluture unsubscribes from Futures that it forksfor us, when it nolonger needs the result. For example, both Futures passed intoraceare forked, but once one of them produces a result, the other is unsubscribedfrom, triggering cancellation. This means that generally, unsubscription andcancellation is fully managed for us behind the scenes.
Fluture interprets our transformations in a stack safe way.This means that none of the following operations result in aRangeError: Maximum call stack size exceeded
:
>constadd1=x=>x+1>letm=resolve(1)>for(leti=0;i<100000;i++){.m=map(add1)(m).}>fork(log('rejection'))(log('resolution'))(m)[resolution]:100001
>constm=(functionrecur(x){.constmx=resolve(x+1).returnx<100000 ?chain(recur)(mx) :mx.}(1))>fork(log('rejection'))(log('resolution'))(m)[resolution]:100001
To learn more about memory and stack usage under different types of recursion,see (or execute)scripts/test-mem
.
First and foremost, Fluture type-checks all of its input and throws TypeErrorswhen incorrect input is provided. The messages they carry are designed toprovide enough insight to figure out what went wrong.
Secondly, Fluture catches exceptions that are thrown asynchronously, andexposes them to you in one of two ways:
- By throwing an Error when it happens.
- By calling yourexception 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 theoriginal 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 thestack
property, and you generally don't need to look at it. If debug modeis not enabled, the list is always empty.stack
: The stack trace of the original exception if it had one, or theError'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 ofFuture
that was beingconsumed when the exception happened. Oftenprinting it as a String can yield usefulinformation. You can also try to consume it in isolation to better identifywhat's going wrong.
Finally, as mentioned, Fluture has adebug mode whereinadditional contextual information across multiple JavaScript ticks iscollected, included as an extended "async stack trace" on Errors, andexposed 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.
There are multiple ways to print a Future to String. Let's take a simplecomputation as an example:
constadd=a=>b=>a+b;consteventualAnswer=ap(resolve(22))(map(add)(resolve(20)));
Casting it to String directly by calling
String(eventualAnswer)
oreventualAnswer.toString()
will yield an approximation of the code thatwas used to create the Future. In this case:"ap (resolve (22)) (map (a => b => a + b) (resolve (20)))"
Casting it to String using
JSON.stringify(eventualAnswer, null, 2)
willyield a kind of abstract syntax tree.{"$":"fluture/Future@5","kind":"interpreter","type":"transform","args": [ {"$":"fluture/Future@5","kind":"interpreter","type":"resolve","args": [20 ] }, [ {"$":"fluture/Future@5","kind":"transformation","type":"ap","args": [ {"$":"fluture/Future@5","kind":"interpreter","type":"resolve","args": [22 ] } ] }, {"$":"fluture/Future@5","kind":"transformation","type":"map","args": [null ] } ] ]}
When using this module withSanctuary Def (andSanctuary byextension) one might run into the following issue:
>importSfrom'sanctuary'>import{resolve}from'fluture'>S.I(resolve(1))!TypeError:Sincethereisnotypeofwhichalltheabovevaluesaremembers,.thetype-variableconstrainthasbeenviolated.
This happens because Sanctuary Def needs to know about the types created byFluture to determine whether the type-variables are consistent.
To let Sanctuary know about these types, we can obtain the type definitionsfromfluture-sanctuary-types
and pass them toS.create
:
>importsanctuaryfrom'sanctuary'>import{envasflutureEnv}from'fluture-sanctuary-types'>import{resolve}from'fluture'>constS=sanctuary.create({checkTypes:true,env:sanctuary.env.concat(flutureEnv)})>fork(log('rejection')).(log('resolution')).(S.I(resolve(42)))[resolution]:42
Most versions of Fluture understand how to consume instances from most otherversions, even across Fluture's major releases. This allows for differentpackages that depend on Fluture to interact.
However, sometimes it's unavoidable that a newer version of Fluture is releasedthat can no longer understand older versions, and vice-versa. This only everhappens on a major release, and will be mentioned in the breaking change log.When two incompatible versions of Fluture meet instances, they do their best toissue a clear error message about it.
When this happens, you need to manually convert the older instance to a newerinstance of Future. WhenisFuture
returnsfalse
, a conversionis necessary. You can also apply this trick if the Future comes from anotherlibrary similar to Fluture.
constNoFuture=require('incompatible-future')constincompatible=NoFuture.of('Hello')constcompatible=Future((rej,res)=>{returnNoFuture.fork(rej)(res)(incompatible)})both(compatible)(resolve('world'))
Future:: ((a->Undefined,b->Undefined)->Cancel)->Futureab
Creates a Future with the given computation. A computation is a function whichtakes two callbacks. Both are continuations for the computation. The first isreject
, commonly abbreviated torej
; The second isresolve
, orres
.When the computation is finished (possibly asynchronously) it may call theappropriate continuation with a failure or success value.
Additionally, the computation must return a nullary function containingcancellation logic. SeeCancellation.
If you find that there is no way to cancel your computation, you can return anoop
function as a cancellation function. However, at this point there isusually a more fitting way tocreate that Future(like for example vianode
).
>fork(log('rejection')).(log('resolution')).(Future(functioncomputation(reject,resolve){.constt=setTimeout(resolve,20,42).return()=>clearTimeout(t).}))[resolution]:42
resolve::b->Futureab
Creates a Future which immediately resolves with the given value.
>fork(log('rejection')).(log('resolution')).(resolve(42))[answer]:42
reject::a->Futureab
Creates a Future which immediately rejects with the given value.
>fork(log('rejection')).(log('resolution')).(reject('It broke!'))[rejection]:"It broke!"
after::Number->b->Futureab
Creates a Future which resolves with the given value afterthe given number of milliseconds.
>fork(log('rejection')).(log('resolution')).(after(20)(42))[resolution]:42
rejectAfter::Number->a->Futureab
Creates a Future which rejects with the given reason after the given number ofmilliseconds.
>fork(log('rejection')).(log('resolution')).(rejectAfter(20)('It broke!'))[rejection]:"It broke!"
go:: (()->Iterator)->Futureab
A way to doasync
/await
with Futures, similar to Promise Coroutines orHaskell Do-notation.
Takes a function which returns anIterator, commonly agenerator-function, and chains every produced Future over the previous.
>fork(log('rejection'))(log('resolution'))(go(function*(){.constthing=yieldafter(20)('world').constmessage=yieldafter(20)('Hello '+thing).returnmessage+'!'.}))[resolution]:"Hello world!"
A rejected Future short-circuits the whole coroutine.
>fork(log('rejection'))(log('resolution'))(go(function*(){.constthing=yieldreject('It broke!').constmessage=yieldafter(20)('Hello '+thing).returnmessage+'!'.}))[rejection]:"It broke!"
To handle rejectionsinside the coroutine, we need tocoalesce
the error into our control domain.
I recommend using coalesce with anEither
.
>constcontrol=coalesce(S.Left)(S.Right)>fork(log('rejection'))(log('resolution'))(go(function*(){.constthing=yieldcontrol(reject('It broke!')).returnS.either(x=>`Oh no!${x}`).(x=>`Yippee!${x}`).(thing).}))[resolution]:"Oh no! It broke!"
attempt::ThrowingeUndefinedr->Futureer
Creates a Future which resolves with the result of calling the given function,or rejects with the error thrown by the given function.
Short forencase (f) (undefined)
.
>constdata={foo:'bar'}>fork(log('rejection')).(log('resolution')).(attempt(()=>data.foo.bar.baz))[rejection]:newTypeError("Cannot read property 'baz' of undefined")
attemptP:: (Undefined->Promiseab)->Futureab
Create a Future which when forked spawns a Promise using the given function andresolves with its resolution value, or rejects with its rejection reason.
Short forencaseP (f) (undefined)
.
>fork(log('rejection')).(log('resolution')).(attemptP(()=>Promise.resolve(42)))[resolution]:42
node:: (Nodebacker->x)->Futureer
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 functiondoes not support cancellation.
>fork(log('rejection')).(log('resolution')).(node(done=>done(null,42)))[resolution]:42
encase::Throwingear->a->Futureer
Takes a function and a value, and returns a Future which when forked calls thefunction with the value and resolves with the result. If the function throwsan exception, it is caught and the Future will reject with the exception.
Applyingencase
with a functionf
creates a "safe" version off
. Insteadof throwing exceptions, the encased version always returns a Future.
>fork(log('rejection')).(log('resolution')).(encase(JSON.parse)('{"foo" = "bar"}'))[rejection]:newSyntaxError('Unexpected token =')
encaseP:: (a->Promiseer)->a->Futureer
Turns Promise-returning functions 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 thePromise, and resolves with its resolution value, or rejects with its rejectionreason.
>encaseP(fetch)('https://api.github.com/users/Avaq')..pipe(chain(encaseP(res=>res.json())))..pipe(map(user=>user.name))..pipe(fork(log('rejection'))(log('resolution')))[resolution]:"Aldwin Vlasblom"
map::Functorm=> (a->b)->ma->mb
Transforms the resolution value inside the Future orFunctor,and returns a Future or Functor with the new value. The transformation is onlyapplied to the resolution branch: if the Future is rejected, the transformationis ignored.
>fork(log('rejection')).(log('resolution')).(map(x=>x+1)(resolve(41)))[resolution]:42
For comparison, an approximation with Promises is:
>Promise.resolve(41)..then(x=>x+1)..then(log('resolution'),log('rejection'))[resolution]:42
bimap::Bifunctorm=> (a->c)-> (b->d)->mab->mcd
Maps the left function over the rejection reason, or the right function overthe resolution value, depending on which is present. Can be used on anyBifunctor.
>fork(log('rejection')).(log('resolution')).(bimap(x=>x+'!')(x=>x+1)(resolve(41)))[resolution]:42>fork(log('rejection')).(log('resolution')).(bimap(x=>x+'!')(x=>x+1)(reject('It broke!')))[rejection]:"It broke!!"
For comparison, an approximation with Promises is:
>Promise.resolve(41)..then(x=>x+1,x=>Promise.reject(x+'!'))..then(log('resolution'),log('rejection'))[resolution]:42>Promise.reject('It broke!')..then(x=>x+1,x=>Promise.reject(x+'!'))..then(log('resolution'),log('rejection'))[rejection]:"It broke!!"
chain::Chainm=> (a->mb)->ma->mb
Sequence a new Future orChain using the resolution value fromanother. Similarly tomap
,chain
expects a function. But insteadof returning the newvalue, chain expects a Future (or instance of the sameChain) to be returned.
The transformation is only applied to the resolution branch: if the Future isrejected, the transformation is ignored.
See alsochainRej
.
>fork(log('rejection')).(log('resolution')).(chain(x=>resolve(x+1))(resolve(41)))[resolution]:42
For comparison, an approximation with Promises is:
>Promise.resolve(41)..then(x=>Promise.resolve(x+1))..then(log('resolution'),log('rejection'))[resolution]:42
bichain:: (a->Futurecd)-> (b->Futurecd)->Futureab->Futurecd
Sequence a new Future using either the resolution or the rejection value fromanother. Similarly tobimap
,bichain
expects two functions. Butinstead of returning the newvalue, bichain expects Futures to be returned.
>fork(log('rejection')).(log('resolution')).(bichain(resolve)(x=>resolve(x+1))(resolve(41)))[resolution]:42>fork(log('rejection')).(log('resolution')).(bichain(x=>resolve(x+1))(resolve)(reject(41)))[resolution]:42
For comparison, an approximation with Promises is:
>Promise.resolve(41)..then(x=>Promise.resolve(x+1),Promise.resolve)..then(log('resolution'),log('rejection'))[resolution]:42>Promise.reject(41)..then(Promise.resolve,x=>Promise.resolve(x+1))..then(log('resolution'),log('rejection'))[resolution]:42
swap::Futureab->Futureba
Swap the rejection and resolution branches.
>fork(log('rejection')).(log('resolution')).(swap(resolve(42)))[rejection]:42>fork(log('rejection')).(log('resolution')).(swap(reject(42)))[resolution]:42
mapRej:: (a->c)->Futureab->Futurecb
Map over therejection reason of the Future. This is likemap
,but for the rejection branch.
>fork(log('rejection')).(log('resolution')).(mapRej(s=>`Oh no!${s}`)(reject('It broke!')))[rejection]:"Oh no! It broke!"
For comparison, an approximation with Promises is:
>Promise.reject('It broke!')..then(null,s=>Promise.reject(`Oh no!${s}`))..then(log('resolution'),log('rejection'))[rejection]:"Oh no! It broke!"
chainRej:: (a->Futurecb)->Futureab->Futurecb
Chain over therejection reason of the Future. This is likechain
, but for the rejection branch.
>fork(log('rejection')).(log('resolution')).(chainRej(s=>resolve(`${s} But it's all good.`))(reject('It broke!')))[resolution]:"It broke! But it's all good."
For comparison, an approximation with Promises is:
>Promise.reject('It broke!')..then(null,s=>`${s} But it's all good.`)..then(log('resolution'),log('rejection'))[resolution]:"It broke! But it's all good."
coalesce:: (a->c)-> (b->c)->Futureab->Futuredc
Applies the left function to the rejection value, or the right function to theresolution value, depending on which is present, and resolves with the result.
This provides a convenient means to ensure a Future is always resolved. It canbe used with other type constructors, likeS.Either
, to maintaina representation of failure.
>fork(log('rejection')).(log('resolution')).(coalesce(S.Left)(S.Right)(resolve('hello'))[resolution]:Right("hello")>fork(log('rejection')).(log('resolution')).(coalesce(S.Left)(S.Right)(reject('It broke!'))[resolution]:Left("It broke!")
For comparison, an approximation with Promises is:
>Promise.resolve('hello')..then(S.Right,S.Left)..then(log('resolution'),log('rejection'))[resolution]:Right("hello")>Promise.reject('It broke!')..then(S.Right,S.Left)..then(log('resolution'),log('rejection'))[resolution]:Left("It broke!")
ap::Applym=>ma->m (a->b)->mb
Applies the function contained in the right-hand Future orApplyto the value contained in the left-hand Future or Apply. This process can berepeated to gradually fill out multiple function arguments of a curriedfunction, as shown below.
Note that the Futures will be executed in sequence - not in parallel* -because of the Monadic nature of Futures. The execution order is, asspecified by Fantasy Land,m (a -> b)
first followed bym a
.So that'sright before left.
* Have a look atpap
for anap
function that runs its argumentsin parallel. If you must useap
(because you're creating a generalizedfunction), but still want Futures passed into it to run in parallel, thenyou could useConcurrentFuture instead.
>fork(log('rejection')).(log('resolution')).(ap(resolve(7))(ap(resolve(49))(resolve(x=>y=>x-y))))[resolution]:42
pap::Futureab->Futurea (b->c)->Futureac
Has the same signature and function asap
, but runs the two Futuresgiven to it in parallel. See alsoConcurrentFuture for amore general way to achieve this.
>fork(log('rejection')).(log('resolution')).(pap(resolve(7))(pap(resolve(49))(resolve(x=>y=>x-y))))[resolution]:42
alt::Altf=>fa->fa->fa
Select one of twoAlts.
Behaves like logicalor onFuture
instances, returning a newFuture which either resolves with the first resolution value, or rejects withthe last rejection reason. We can use it if we want a computation to run onlyif another has failed.
Note that the Futures will be executed in sequence - not in parallel* -because of the Monadic nature of Futures. Theright Future is evaluatedbefore theleft Future.
* If you'd like to use a parallel implementation ofalt
, you could simplyuserace
. Alternatively you could wrap your Future instanceswithPar
before passing them toalt
.
>fork(log('rejection')).(log('resolution')).(alt(resolve('left'))(resolve('right')))[resolution]:"right">fork(log('rejection')).(log('resolution')).(alt(resolve('left'))(reject('It broke!')))[resolution]:"left"
and::Futureac->Futureab->Futureac
Logicaland for Futures.
Returns a new Future which either rejects with the first rejection reason, orresolves with the last resolution value once and if both Futures resolve. Wecan use it if we want a computation to run only after another has succeeded.Theright Future is evaluated before theleft Future.
>fork(log('rejection')).(log('resolution')).(and(resolve('left'))(resolve('right')))[resolution]:"left">fork(log('rejection')).(log('resolution')).(and(resolve('left'))(reject('It broke!')))[rejection]:"It broke!"
lastly::Futureac->Futureab->Futureab
Run a second Future after the first settles (successfully or unsuccessfully).Rejects with the rejection reason from the first or second Future, or resolveswith the resolution value from the first Future. This can be used to run acomputation after another settles, successfully or unsuccessfully.
If you're looking to clean up resources after running a computation whichacquires them, you should usehook
, which has many more fail-safesin place.
>fork(log('rejection')).(log('resolution')).(lastly(encase(log('lastly'))('All done!'))(resolve(42)))[lastly]:"All done!"[resolution]:42
fork:: (a->Any)-> (b->Any)->Futureab->Cancel
Execute the computation represented by a Future, passingreject
andresolve
callbacks to continue once there is a result.
This function is calledfork
because it literally represents a fork in ourprogram: a point where a single code-path splits in two. It is recommended tokeep the number of calls tofork
at a minimum for this reason. The moreforks, the higher the code complexity.
Generally, one only needs to callfork
in a single place in the entireprogram.
After wefork
a Future, the computation will start running. If the programdecides halfway through that it's no longer interested in the result of thecomputation, it can call theunsubscribe
function returned byfork
. SeeCancellation.
If an exception was encountered during the computation, it will be re-thrownbyfork
and likely not be catchable. You can handle it usingprocess.on('uncaughtException')
in Node, or useforkCatch
.
Almost all code examples in Fluture usefork
to run the computation. Thereare some variations onfork
that serve different purposes below.
forkCatch:: (Error->Any)-> (a->Any)-> (b->Any)->Futureab->Cancel
An advanced version offork that allows us to react to a fatal errorin a custom way. Fatal errors occur when unexpected exceptions are thrown, whenthe Fluture API is used incorrectly, or when resources couldn't be disposed.
The exception handler will always be called with an instance ofError
,independent of what caused the crash.
Using this function is a trade-off;
Generally it's best to let a program crash and restart when an a fatal erroroccurs. Restarting is the surest way to restore the memory that was allocatedby the program to an expected state.
By usingforkCatch
, we can keep our program alive after a fatal error, whichcan be very beneficial when the program is being used by multiple clients.However, since fatal errors might indicate that something, somewhere hasentered an invalid state, it's probably still best to restart our program uponencountering one.
SeeDebugging for information about the Error object that ispassed to your exception handler.
>forkCatch(log('fatal error')).(log('rejection')).(log('resolution')).(map(x=>x.foo)(resolve(null)))[fatalerror]:newError("Cannot read property 'foo' of null")
value:: (b->Any)->Futureab->Cancel
Likefork
but for the resolution branch only. Only use this functionif you are sure the Future is going to be resolved, for example; after usingcoalesce
. If the Future rejects,value
will throw an Error.
As withfork
,value
returns anunsubscribe
function. SeeCancellation.
>value(log('resolution'))(resolve(42))[resolution]:42
done::Nodebackab->Futureab->Cancel
Run the Future using aNodeback as the continuation.
This is likefork
, but instead of taking two unary functions, ittakes a single binary function.
As withfork
,done
returns anunsubscribe
function. SeeCancellation.
>done((err,val)=>log('resolution')(val))(resolve(42))[resolution]:42
promise::FutureErrora->PromiseErrora
Run the Future and get a Promise to represent its continuation.
Returns a Promise which resolves with the resolution value, or rejects withthe rejection reason of the Future.
If an exception was encountered during the computation, the promise will rejectwith it. I recommend usingcoalesce
beforepromise
to ensurethat exceptions and rejections are not mixed into the Promise rejection branch.
Cancellation capabilities are lost when usingpromise
to consume the Future.
>promise(resolve(42)).then(log('resolution'))[resolution]:42>promise(reject('failure')).then(log('resolution'),log('rejection'))[rejection]:"failure"
race::Futureab->Futureab->Futureab
Race two Futures against each other. Creates a new Future which resolves orrejects with the resolution or rejection value of the first Future to settle.
When one Future settles, the other gets cancelled automatically.
>fork(log('rejection')).(log('resolution')).(race(after(15)('left'))(after(30)('right')))[resolution]:"left"
both::Futureab->Futureac->Futurea (Pairbc)
Run two Futures in parallel and get aPair
of the results. Wheneither Future rejects, the other Future will be cancelled and the resultingFuture will reject.
>fork(log('rejection')).(log('resolution')).(both(after(15)('left'))(after(30)('right')))[resolution]:["left","right"]
parallel::PositiveInteger->Array (Futureab)->Futurea (Arrayb)
Creates a Future which when forked runs all Futures in the given Array inparallel, ensuring no more thanlimit
Futures are running at once.
In the following example, we're running up to 5 Futures in parallel. EveryFuture takes about 20ms to settle, which means the result should appear afterabout 40ms.
If we use1
for the limit, the Futures would run in sequence, causing theresult to appear only after 200ms.
We can also useInfinity
as the limit. This would create a function similartoPromise.all
, which always runs all Futures in parallel. This can easilycause the computation to consume too many resources, however, so I wouldadvise using a number roughly equal to maximum size of Array you think yourprogram should handle.
>fork(log('rejection')).(log('resolution')).(parallel(5)(Array.from(Array(10).keys()).map(after(20))))[resolution]:[0,1,2,3,4,5,6,7,8,9]
When one Future rejects, all currently running Futures will be cancelled andthe resulting Future will reject. If you want to settle all Futures, even ifsome may fail, you can useparallel
in combination withcoalesce.
>fork(log('rejection')).(log('resolution')).(parallel(2)([resolve(42),reject('It broke!')]..map(coalesce(S.Left)(S.Right))))[resolution]:[Right(42),Left("It broke!")]
TheConcurrentFuture
type is very similar to theFuture
type, except thatit hasparallel semantics whereFuture
hassequential semantics.
These sematics are most notable in the implementation of Applicative forConcurrentFuture
. When usingap
on two ConcurrentFutures, theyrun parallely, whereas regularFuture
instances would've run sequentially.This means thatConcurrentFuture
cannot be a Monad, which is why we haveit as a separate type.
The implementation of Alternative onConcurrentFuture
has parallel semanticsas well. Whereasalt
on regular Futures uses the failure effect todetermine a winner, on ConcurrentFuturestiming is used, and the winner willbe whichever ConcurrentFuture settled first.
The idea is that we can switch back and forth betweenFuture
andConcurrentFuture
, usingPar
andseq
, to get sequential orconcurrent behaviour respectively. It's a useful type to pass to abstractionsthat don't know about Future-specific functions likeparallel
orrace
, butdo know how to operate on Apply and Alternative.
//Some dummy valuesconstx=41;constf=a=>a+1;//The following two are equal ways to construct a ConcurrentFutureconstparx=S.of(Par)(x)constparf=Par(S.of(Future)(f))//We can make use of parallel applyvalue(log('resolution'))(seq(ap(parx)(parf)))[resolution]:42//Concurrent sequencingvalue(log('resolution'))(seq(S.sequence(Par)([parx,parx,parx])))[resolution]:[41,41,41]//And concurrent altvalue(log('resolution'))(alt(after(15)('left'))(after(30)('right')))[resolution]:"left"
Par::Futureab->ConcurrentFutureab
Converts a Future to a ConcurrentFuture.
Converts a ConcurrentFuture to a Future.
seq::ConcurrentFutureab->Futureab
Functions listed under this category allow for more fine-grained control overthe flow of acquired values.
hook::Futureab-> (b->Futurecd)-> (b->Futureae)->Futureae
Combines resource acquisition, consumption, and disposal in such a way that youcan be sure that a resource will always be disposed if it was acquired, even ifan exception is thrown during consumption; Sometimes referred to as bracketing.
The signature is likehook (acquire, dispose, consume)
, where:
acquire
is a Future which might create connections, open files, etc.dispose
is a function that takes the result fromacquire
and should beused to clean up (close connections etc). The Future it returns mustresolve, and its resolution value is ignored. If it rejects, a fatal erroris raised which can only be handled withforkCatch
.consume
is another Function takes the result fromacquire
, and may beused to perform any arbitrary computations using the resource.
Typically, you'd want to partially apply this function with the first twoarguments (acquisition and disposal), as shown in the example.
>import{open,read,close}from'fs'>constwithFile=hook(node(done=>open('package.json','r',done))).(fd=>node(done=>close(fd,done)))>fork(log('rejection')).(log('resolution')).(withFile(fd=>node(done=>(.read(fd,Buffer.alloc(1),0,1,null,(e,_,x)=>done(e,x))).)))[resolution]:<Buffer7b>
When a hooked Future is cancelled while acquiring its resource, nothing elsewill happen. When it's cancelled after acquistion completes, however, thedisposal will still run, and if it fails, an exception will be thrown.
If you have multiple resources that you'd like to consume all at once, you canuseFluture Hooks to combinemultiple hooks into one.
Future.prototype.pipe::Futureab~> (Futureab->c)->c
A method available on all Futures to allow arbitrary functions over Futures tobe included in a fluent-style method chain.
You can think of this as a fallback for theESNext pipe operator (|>
).
>resolve(x=>y=>x*y)..pipe(ap(after(20)(Math.PI)))..pipe(ap(after(20)(13.37)))..pipe(map(Math.round))..pipe(fork(log('rejection'))(log('resolution')))[resolution]:42
cache::Futureab->Futureab
Returns a Future which caches the resolution value or rejection reason of thegiven Future so that whenever it's forked, it can load the value from cacherather than re-executing the underlying computation.
This essentially turns a unicast Future into a multicast Future, allowingmultiple consumers to subscribe to the same result. The underlying computationis nevercancelled unlessall consumers unsubscribe beforeit completes.
There is a glaring drawback to usingcache
, which is that returnedFutures are no longer referentially transparent, making reasoning about themmore difficult and refactoring code that uses them harder.
>import{readFile}from'fs'>consteventualPackageName=(.node(done=>readFile('package.json','utf8',done))..pipe(chain(encase(JSON.parse)))..pipe(chain(encase(x=>x.name)))..pipe(map(data=>{.log('debug')('Read, parsed, and traversed the package data').returndata.})).)>fork(log('rejection'))(log('resolution'))(eventualPackageName)[debug]:"Read, parsed, and traversed the package data"[resolution]:"Fluture">fork(log('rejection'))(log('resolution'))(eventualPackageName)[debug]:"Read, parsed, and traversed the package data"[resolution]:"Fluture">consteventualCachedPackageName=cache(eventualPackageName)>fork(log('rejection'))(log('resolution'))(eventualCachedPackageName)[debug]:"Read, parsed, and traversed the package data"[resolution]:"Fluture">fork(log('rejection'))(log('resolution'))(eventualCachedPackageName)[resolution]:"Fluture"
isFuture::a->Boolean
Returns true forFutures and false for everything else. This function(andS.is
) also returntrue
for instances of Future that werecreated within other contexts. It is therefore recommended to use this overinstanceof
, unless your intent is to explicitly check for Futures createdusing the exactFuture
constructor you're testing against.
>isFuture(resolve(42))true>isFuture(42)false
never::Futureab
A Future that never settles. Can be useful as an initial value when reducingwithrace
, for example.
isNever::a->Boolean
Returnstrue
if the given input is anever
.
extractLeft::Futureab->Arraya
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, thearray will be empty. This function is meant to be used for type introspection:it isnot the correct way toconsume a Future.
extractRight::Futureab->Arrayb
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, thearray will be empty. This function is meant to be used for type introspection:it isnot the correct way toconsume a Future.
debugMode::Boolean->Undefined
Enable or disable Fluture's debug mode. Debug mode is disabled by default.Passtrue
to enable, orfalse
to disable.
debugMode(true)
For more information, seeDebugging andContext.
Future.prototype.context::Futureab~>ListContext
A linked list of debugging contexts made available on every instance ofFuture
. Whendebug mode is disabled, the list is always empty.
The context objects havestack
properties which contain snapshots of thestacktraces leading up to the creation of theFuture
instance. They are usedby Fluture to generate contextual stack traces.
About
🦋 Fantasy Land compliant (monadic) alternative to Promises