Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

🦋 Fantasy Land compliant (monadic) alternative to Promises

License

NotificationsYou must be signed in to change notification settings

fluture-js/Fluture

Repository files navigation

Fluture

Build StatusCode CoverageDependency StatusNPM PackageGitter Chat

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:

For more information:

Installation

With NPM

$npm install --save fluture

Bundled from a CDN

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.

Usage

EcmaScript Module

Fluture is written as modular JavaScript.

  • On Node 12 and up, Fluture can be loaded directly withimport '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))

CommonJS Module

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))

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

Butterfly

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.

Interoperability

Fantasy Land

  • 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.

Type signatures

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.

Types

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 typea 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 withnext-methods which conform to theIterator protocol.
  • Cancel - The nullarycancellation functions returned from computations.
  • Throwing e a b - A function froma 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 }.

Type classes

Some signatures containconstrained type variables.Generally, these constraints express that some value must conform to aFantasy Land-specified interface.

Cancellation

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.

Stack safety

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.

Debugging

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:

  1. By throwing an Error when it happens.
  2. 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.

Casting Futures to String

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)));
  1. Casting it to String directly by callingString(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)))"
  2. Casting it to String usingJSON.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        ]      }    ]  ]}

Sanctuary

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

Incompatible Fluture Versions

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'))

Creating Futures

Future

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

resolve::b->Futureab

Creates a Future which immediately resolves with the given value.

>fork(log('rejection')).(log('resolution')).(resolve(42))[answer]:42

reject

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

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

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

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 tocoalescethe 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

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

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

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

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

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"

Transforming Futures

map

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.

See alsochain andmapRej.

>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

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

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

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

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

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

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

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!")

Combining Futures

ap

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

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

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.

See alsoand andlastly.

* 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

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.

See alsoalt andlastly.

>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

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.

See alsoand andalt.

>fork(log('rejection')).(log('resolution')).(lastly(encase(log('lastly'))('All done!'))(resolve(42)))[lastly]:"All done!"[resolution]:42

Consuming Futures

fork

fork:: (a->Any)-> (b->Any)->Futureab->Cancel

Execute the computation represented by a Future, passingreject andresolvecallbacks 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

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

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

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

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"

Parallelism

race

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

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

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!")]

ConcurrentFuture

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
Par::Futureab->ConcurrentFutureab

Converts a Future to a ConcurrentFuture.

seq

Converts a ConcurrentFuture to a Future.

seq::ConcurrentFutureab->Futureab

Resource management

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

hook

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.

Utility functions

pipe

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

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

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

never::Futureab

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

isNever

isNever::a->Boolean

Returnstrue if the given input is anever.

extractLeft

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

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

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.

context

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.

License

MIT licensed


[8]ページ先頭

©2009-2025 Movatter.jp