UNPKG

fluture

Version:

FantasyLand compliant (monadic) alternative to Promises

499 lines (421 loc) 15.4 kB
/*eslint no-cond-assign:0, no-constant-condition:0 */ import type from 'sanctuary-type-identifiers'; import {FL, $$type} from './internal/const.js'; import {captureContext, captureApplicationContext, captureStackTrace} from './internal/debug.js'; import { invalidArgument, invalidArgumentOf, invalidArity, invalidFuture, invalidFutureArgument, typeError, withExtraContext, wrapException } from './internal/error.js'; import {Next, Done} from './internal/iteration.js'; import {nil, cons, isNil, reverse, toArray} from './internal/list.js'; import {isFunction, isUnsigned} from './internal/predicates.js'; import {show, noop, call, moop} from './internal/utils.js'; function alwaysTrue(){ return true; } function getArgs(it){ var args = new Array(it.arity); for(var i = 1; i <= it.arity; i++){ args[i - 1] = it['$' + String(i)]; } return args; } function showArg(arg){ return ' (' + show(arg) + ')'; } export var any = {pred: alwaysTrue, error: invalidArgumentOf('be anything')}; export var func = {pred: isFunction, error: invalidArgumentOf('be a Function')}; export var future = {pred: isFuture, error: invalidFutureArgument}; export var positiveInteger = {pred: isUnsigned, error: invalidArgumentOf('be a positive Integer')}; export function application(n, f, type, args, prev){ if(args.length < 2 && type.pred(args[0])) return captureApplicationContext(prev, n, f); var e = args.length > 1 ? invalidArity(f, args) : type.error(f.name, n - 1, args[0]); captureStackTrace(e, f); throw withExtraContext(e, prev); } export function application1(f, type, args){ return application(1, f, type, args, nil); } export function Future(computation){ var context = application1(Future, func, arguments); return new Computation(context, computation); } export function isFuture(x){ return x instanceof Future || type(x) === $$type; } // Compliance with sanctuary-type-identifiers versions 1 and 2. // To prevent sanctuary-type-identifiers version 3 from identifying 'Future' // as being of the type denoted by $$type, we ensure that // Future.constructor.prototype is equal to Future. Future['@@type'] = $$type; Future.constructor = {prototype: Future}; Future[FL.of] = resolve; Future[FL.chainRec] = chainRec; Future.prototype['@@type'] = $$type; Future.prototype['@@show'] = function Future$show(){ return this.toString(); }; Future.prototype.pipe = function Future$pipe(f){ if(!isFunction(f)) throw invalidArgument('Future#pipe', 0, 'be a Function', f); return f(this); }; Future.prototype[FL.ap] = function Future$FL$ap(other){ var context = captureContext(nil, 'a Fantasy Land dispatch to ap', Future$FL$ap); return other._transform(new ApTransformation(context, this)); }; Future.prototype[FL.map] = function Future$FL$map(mapper){ var context = captureContext(nil, 'a Fantasy Land dispatch to map', Future$FL$map); return this._transform(new MapTransformation(context, mapper)); }; Future.prototype[FL.bimap] = function Future$FL$bimap(lmapper, rmapper){ var context = captureContext(nil, 'a Fantasy Land dispatch to bimap', Future$FL$bimap); return this._transform(new BimapTransformation(context, lmapper, rmapper)); }; Future.prototype[FL.chain] = function Future$FL$chain(mapper){ var context = captureContext(nil, 'a Fantasy Land dispatch to chain', Future$FL$chain); return this._transform(new ChainTransformation(context, mapper)); }; Future.prototype[FL.alt] = function Future$FL$alt(other){ var context = captureContext(nil, 'a Fantasy Land dispatch to alt', Future$FL$alt); return this._transform(new AltTransformation(context, other)); }; Future.prototype.extractLeft = function Future$extractLeft(){ return []; }; Future.prototype.extractRight = function Future$extractRight(){ return []; }; Future.prototype._transform = function Future$transform(transformation){ return new Transformer(transformation.context, this, cons(transformation, nil)); }; Future.prototype.isTransformer = false; Future.prototype.context = nil; Future.prototype.arity = 0; Future.prototype.name = 'future'; Future.prototype.toString = function Future$toString(){ return this.name + getArgs(this).map(showArg).join(''); }; Future.prototype.toJSON = function Future$toJSON(){ return {$: $$type, kind: 'interpreter', type: this.name, args: getArgs(this)}; }; export function createInterpreter(arity, name, interpret){ var Interpreter = function(context, $1, $2, $3){ this.context = context; this.$1 = $1; this.$2 = $2; this.$3 = $3; }; Interpreter.prototype = Object.create(Future.prototype); Interpreter.prototype.arity = arity; Interpreter.prototype.name = name; Interpreter.prototype._interpret = interpret; return Interpreter; } export var Computation = createInterpreter(1, 'Future', function Computation$interpret(rec, rej, res){ var computation = this.$1, open = false, cancel = noop, cont = function(){ open = true }; try{ cancel = computation(function Computation$rej(x){ cont = function Computation$rej$cont(){ open = false; rej(x); }; if(open){ cont(); } }, function Computation$res(x){ cont = function Computation$res$cont(){ open = false; res(x); }; if(open){ cont(); } }); }catch(e){ rec(wrapException(e, this)); return noop; } if(!(isFunction(cancel) && cancel.length === 0)){ rec(wrapException(typeError( 'The computation was expected to return a nullary cancellation function\n' + ' Actual: ' + show(cancel) ), this)); return noop; } cont(); return function Computation$cancel(){ if(open){ open = false; cancel && cancel(); } }; }); export var Never = createInterpreter(0, 'never', function Never$interpret(){ return noop; }); Never.prototype._isNever = true; export var never = new Never(nil); export function isNever(x){ return isFuture(x) && x._isNever === true; } export var Crash = createInterpreter(1, 'crash', function Crash$interpret(rec){ rec(this.$1); return noop; }); export function crash(x){ return new Crash(application1(crash, any, arguments), x); } export var Reject = createInterpreter(1, 'reject', function Reject$interpret(rec, rej){ rej(this.$1); return noop; }); Reject.prototype.extractLeft = function Reject$extractLeft(){ return [this.$1]; }; export function reject(x){ return new Reject(application1(reject, any, arguments), x); } export var Resolve = createInterpreter(1, 'resolve', function Resolve$interpret(rec, rej, res){ res(this.$1); return noop; }); Resolve.prototype.extractRight = function Resolve$extractRight(){ return [this.$1]; }; export function resolve(x){ return new Resolve(application1(resolve, any, arguments), x); } //Note: This function is not curried because it's only used to satisfy the // Fantasy Land ChainRec specification. export function chainRec(step, init){ return resolve(Next(init))._transform(new ChainTransformation(nil, function chainRec$recur(o){ return o.done ? resolve(o.value) : step(Next, Done, o.value)._transform(new ChainTransformation(nil, chainRec$recur)); })); } export var Transformer = createInterpreter(2, 'transform', function Transformer$interpret(rec, rej, res){ //These are the cold, and hot, transformation stacks. The cold actions are those that //have yet to run parallel computations, and hot are those that have. var cold = nil, hot = nil; //These combined variables define our current state. // future = the future we are currently forking // transformation = the transformation to be informed when the future settles // cancel = the cancel function of the current future // settled = a boolean indicating whether a new tick should start // async = a boolean indicating whether we are awaiting a result asynchronously var future, transformation, cancel = noop, settled, async = true, it; //Takes a transformation from the top of the hot stack and returns it. function nextHot(){ var x = hot.head; hot = hot.tail; return x; } //Takes a transformation from the top of the cold stack and returns it. function nextCold(){ var x = cold.head; cold = cold.tail; return x; } //This function is called with a future to use in the next tick. //Here we "flatten" the actions of another Sequence into our own actions, //this is the magic that allows for infinitely stack safe recursion because //actions like ChainAction will return a new Sequence. //If we settled asynchronously, we call drain() directly to run the next tick. function settle(m){ settled = true; future = m; if(future.isTransformer){ var tail = future.$2; while(!isNil(tail)){ cold = cons(tail.head, cold); tail = tail.tail; } future = future.$1; } if(async) drain(); } //This function serves as a rejection handler for our current future. //It will tell the current transformation that the future rejected, and it will //settle the current tick with the transformation's answer to that. function rejected(x){ settle(transformation.rejected(x)); } //This function serves as a resolution handler for our current future. //It will tell the current transformation that the future resolved, and it will //settle the current tick with the transformation's answer to that. function resolved(x){ settle(transformation.resolved(x)); } //This function is passed into actions when they are "warmed up". //If the transformation decides that it has its result, without the need to await //anything else, then it can call this function to force "early termination". //When early termination occurs, all actions which were stacked prior to the //terminator will be skipped. If they were already hot, they will also be //sent a cancel signal so they can cancel their own concurrent computations, //as their results are no longer needed. function early(m, terminator){ cancel(); cold = nil; if(async && transformation !== terminator){ transformation.cancel(); while((it = nextHot()) && it !== terminator) it.cancel(); } settle(m); } //This will cancel the current Future, the current transformation, and all stacked hot actions. function Sequence$cancel(){ cancel(); transformation && transformation.cancel(); while(it = nextHot()) it.cancel(); } //This function is called when an exception is caught. function exception(e){ Sequence$cancel(); settled = true; cold = hot = nil; var error = wrapException(e, future); future = never; rec(error); } //This function serves to kickstart concurrent computations. //Takes all actions from the cold stack in reverse order, and calls run() on //each of them, passing them the "early" function. If any of them settles (by //calling early()), we abort. After warming up all actions in the cold queue, //we warm up the current transformation as well. function warmupActions(){ cold = reverse(cold); while(cold !== nil){ it = cold.head.run(early); if(settled) return; hot = cons(it, hot); cold = cold.tail; } transformation = transformation.run(early); } //This function represents our main execution loop. By "tick", we've been //referring to the execution of one iteration in the while-loop below. function drain(){ async = false; while(true){ settled = false; if(transformation = nextCold()){ cancel = future._interpret(exception, rejected, resolved); if(!settled) warmupActions(); }else if(transformation = nextHot()){ cancel = future._interpret(exception, rejected, resolved); }else break; if(settled) continue; async = true; return; } cancel = future._interpret(exception, rej, res); } //Start the execution loop. settle(this); //Return the cancellation function. return Sequence$cancel; }); Transformer.prototype.isTransformer = true; Transformer.prototype._transform = function Transformer$_transform(transformation){ return new Transformer(transformation.context, this.$1, cons(transformation, this.$2)); }; Transformer.prototype.toString = function Transformer$toString(){ return toArray(reverse(this.$2)).reduce(function(str, action){ return action.name + getArgs(action).map(showArg).join('') + ' (' + str + ')'; }, this.$1.toString()); }; function BaseTransformation$rejected(x){ this.cancel(); return new Reject(this.context, x); } function BaseTransformation$resolved(x){ this.cancel(); return new Resolve(this.context, x); } function BaseTransformation$toJSON(){ return {$: $$type, kind: 'transformation', type: this.name, args: getArgs(this)}; } export var BaseTransformation = { rejected: BaseTransformation$rejected, resolved: BaseTransformation$resolved, run: moop, cancel: noop, context: nil, arity: 0, name: 'transform', toJSON: BaseTransformation$toJSON }; function wrapHandler(handler){ return function transformationHandler(x){ var m; try{ m = handler.call(this, x); }catch(e){ return new Crash(this.context, e); } if(isFuture(m)){ return m; } return new Crash(this.context, invalidFuture( this.name + ' expects the return value from the function it\'s given', m, '\n When called with: ' + show(x) )); }; } export function createTransformation(arity, name, prototype){ var Transformation = function(context, $1, $2){ this.context = context; this.$1 = $1; this.$2 = $2; }; Transformation.prototype = Object.create(BaseTransformation); Transformation.prototype.arity = arity; Transformation.prototype.name = name; if(typeof prototype.rejected === 'function'){ Transformation.prototype.rejected = wrapHandler(prototype.rejected); } if(typeof prototype.resolved === 'function'){ Transformation.prototype.resolved = wrapHandler(prototype.resolved); } if(typeof prototype.run === 'function'){ Transformation.prototype.run = prototype.run; } return Transformation; } export var ApTransformation = createTransformation(1, 'ap', { resolved: function ApTransformation$resolved(f){ if(isFunction(f)) return this.$1._transform(new MapTransformation(this.context, f)); throw typeError( 'ap expects the second Future to resolve to a Function\n' + ' Actual: ' + show(f) ); } }); export var AltTransformation = createTransformation(1, 'alt', { rejected: function AltTransformation$rejected(){ return this.$1 } }); export var MapTransformation = createTransformation(1, 'map', { resolved: function MapTransformation$resolved(x){ return new Resolve(this.context, call(this.$1, x)); } }); export var BimapTransformation = createTransformation(2, 'bimap', { rejected: function BimapTransformation$rejected(x){ return new Reject(this.context, call(this.$1, x)); }, resolved: function BimapTransformation$resolved(x){ return new Resolve(this.context, call(this.$2, x)); } }); export var ChainTransformation = createTransformation(1, 'chain', { resolved: function ChainTransformation$resolved(x){ return call(this.$1, x) } });