/********************************************************************** * * * **********************************************/ /* c8 ignore next 2 */ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var object = require('ig-object') var stoppable = require('ig-stoppable') /*********************************************************************/ // NOTE: this is used in a similar fashion to Python's StopIteration... var STOP = module.STOP = stoppable.STOP //--------------------------------------------------------------------- // The generator hierarchy in JS is a bit complicated. // // Consider the following: // // // this is the generator function (i.e. the constructor) // var Iter = function*(lst){ // for(var e of lst){ // yield e }} // // // this is the generator instance (constructed instance)... // var iter = Iter([1,2,3]) // // // In this module we need to add methods to be visible from either Iter // or iter from the above example, so we need to access the prototypes // of each of them. // So, below we will define: // // Generator.prototype // prototype of the generator constructors (i.e. Iter(..) from the // above example) // // Generator.prototype.prototype // generator instance prototype (i.e. iter for the above code) // // // Also the following applies: // // iter instanceof Iter // -> true // // Iter instanceof Generator // // // NOTE: there appears to be no way to test if iter is instance of some // generic Generator... // //--------------------------------------------------------------------- var Generator = module.Generator = (function*(){}).constructor var AsyncGenerator = module.AsyncGenerator = (async function*(){}).constructor // base iterator prototypes... var ITERATOR_PROTOTYPES = [ Array, Set, Map, ].map(function(e){ return (new e()).values().__proto__ }) //--------------------------------------------------------------------- // generic generator wrapper... // helper... var __iter = module.__iter = function*(lst=[]){ if(typeof(lst) == 'object' && Symbol.iterator in lst){ yield* lst } else { yield lst } } // handle stops is onstop(..) is defined... var __onstop = function(res, _, ...args){ var onstop = args.at(-1) typeof(onstop) == 'function' && onstop.call(this, ...(res === STOP ? [] : [res])) } // XXX updatae Array.js' version for compatibility... // XXX DOCS!!! var iter = module.iter = Generator.iter = stoppable( function(lst=[]){ // handler -> generator-constructor... if(typeof(lst) == 'function'){ // we need to be callable... var that = this instanceof Function ? this // generic root generator... : module.__iter return function*(){ yield* that(...arguments).iter(lst) } } // no handler -> generator instance... return module.__iter(lst) }, __onstop) // NOTE: we need .iter(..) to both return generators if passed an iterable // and genereator constructos if passed a function... iter.__proto__ = Generator.prototype //--------------------------------------------------------------------- // Generator.prototype "class" methods... // // the following are effectively the same: // 1) Wrapper // var combined = function(...args){ // return someGenerator(...args) // .filter(function(e){ ... }) // .map(function(e){ ... }) } // // combined( .. ) // // 2) Static generator methods... // var combined = someGenerator // .filter(function(e){ ... }) // .map(function(e){ ... }) // // combined( .. ) // // // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Helpers... // // makeGenerator() // makeGenerator(, ) // -> // // makeGenerator('async', ) // makeGenerator('async', , ) // -> // // // (...args) // -> // // (...inputs) // -> // // (args, ...inputs) // -> args // // // XXX this needs to be of the correct type... (???) // XXX need to accept generators as handlers... var makeGenerator = function(name, pre){ var sync = true if(name == 'async'){ sync = false var [name, pre] = [...arguments].slice(1) } return function(...args){ var that = this return Object.assign( // NOTE: the two branches here are identical, the only // difference is the async keyword... sync ? function*(){ var a = pre ? pre.call(this, args, ...arguments) : args yield* that(...arguments)[name](...a) } : async function*(){ var a = pre ? pre.call(this, args, ...arguments) : args yield* that(...arguments)[name](...a) }, { toString: function(){ return [ that.toString(), // XXX need to normalize args better... `.${ name }(${ args.join(', ') })`, ].join('\n ') }, }) } } // XXX do a better doc... var makeProxy = function(name){ return function(...args){ var that = this return function(){ return that(...arguments)[name](...args) } } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var GeneratorMixin = module.GeneratorMixin = object.Mixin('GeneratorMixin', 'soft', { STOP: object.STOP, iter: module.iter, gat: makeGenerator('gat'), at: function(i){ var that = this return Object.assign( function(){ return that(...arguments).at(i) }, { toString: function(){ return that.toString() + '\n .at('+ i +')'}, }) }, slice: makeGenerator('slice'), flat: makeGenerator('flat'), map: makeGenerator('map'), filter: makeGenerator('filter'), reduce: makeGenerator('reduce'), reduceRight: makeGenerator('reduceRight'), // XXX smap: makeGenerator('smap'), sfilter: makeGenerator('sfilter'), sreduce: makeGenerator('sreduce'), sreduceRight: makeGenerator('sreduceRight'), sforEach: makeGenerator('sforEach'), //*/ between: makeGenerator('between'), // XXX EXPERIMENTAL // XXX add .toString(..) to this??? forEach: function(func){ var that = this return function(){ return that(...arguments).forEach(func) } }, // non-generators... // toArray: function(){ var that = this return Object.assign( function(){ return that(...arguments).toArray() }, { toString: function(){ return that.toString() + '\n .toArray()'}, }) }, gpop: makeGenerator('gpop'), pop: function(){ var that = this return Object.assign( function(){ //return that(...arguments).toArray().pop() }, return that(...arguments).pop() }, { toString: function(){ return that.toString() + '\n .pop()'}, }) }, push: makeGenerator('push'), gshift: makeGenerator('gshift'), shift: function(){ var that = this return Object.assign( function(){ //return that(...arguments).toArray().shift() }, return that(...arguments).shift() }, { toString: function(){ return that.toString() + '\n .shift()'}, }) }, unshift: makeGenerator('unshift'), // promises... // // NOTE: .then(..) and friends are intentionally not defined here to // prevent control deadlocks when awaiting for a generator that // expects manual unwinding e.g.: // g = function*(){} // await g // will hang waiting for g to resolve unwind: makeProxy('unwind'), // combinators... // chain: makeGenerator('chain'), concat: makeGenerator('concat', // initialize arguments... function(next, ...args){ return next .map(function(e){ return (e instanceof Generator || typeof(e) == 'function') ? e(...args) : e }) }), //zip: makeGenerator('zip'), enumerate: makeGenerator('enumerate'), // XXX should this have a .gjoin(..) companion... join: function(){ var args = [...arguments] var that = this return Object.assign( function(){ //return that(...arguments).toArray().shift() }, return that(...arguments).join(...args) }, { toString: function(){ return that.toString() + '\n .join()'}, }) }, }) var GeneratorProtoMixin = module.GeneratorProtoMixin = object.Mixin('GeneratorProtoMixin', 'soft', { // XXX use module.iter(..) ??? iter: stoppable( function*(handler, onerror){ try{ if(handler){ var i = 0 for(var elem of this){ var res = handler.call(this, elem, i) // expand iterables... if(typeof(res) == 'object' && Symbol.iterator in res){ yield* res // as-is... } else { yield res }} // no handler... } else { yield* this } }catch(err){ if(onerror){ if(!(err === STOP || err instanceof STOP)){ var res = onerror(err) if(res){ yield res return } } } throw err }}, __onstop), //*/ at: function(i){ return this.gat(i).next().value }, // XXX this needs the value to be iterable... why??? gat: function*(i){ // sanity check... if(i < 0){ throw new Error('.gat(..): ' +'generator index can\'t be a negative value.')} for(var e of this){ if(i-- == 0){ yield e return } } }, // NOTE: this is different from Array's .slice(..) in that it does not // support negative indexes -- this is done because there is no way // to judge the length of a generator until it is fully done... slice: function*(from=0, to=Infinity){ // sanity check... if(from < 0 || to < 0){ throw new Error('.slice(..): ' +'generator form/to indexes can\'t be negative values.')} var i = 0 for(var e of this){ // stop at end of seq... if(i >= to){ return } // only yield from from... if(i >= from){ yield e } i++ } }, // XXX do we need a version that'll expand generators??? flat: function*(depth=1){ if(depth == 0){ return this } for(var e of this){ // expand array... if(e instanceof Array){ for(var i=0; i < e.length; i++){ if(depth <= 1){ yield e[i] } else { yield* typeof(e[i].flat) == 'function' ? e[i].flat(depth-1) : e[i] } } // item as-is... } else { yield e } } }, // NOTE: if func is instanceof Generator then it's result (iterator) // will be expanded... // NOTE: there is no point to add generator-handler support to either // .filter(..) or .reduce(..) map: stoppable( function*(func){ var i = 0 if(func instanceof Generator){ for(var e of this){ yield* func(e, i++, this) } } else { for(var e of this){ yield func(e, i++, this) } } }, __onstop), smap: function*(func){ yield* this.map(...arguments) }, filter: stoppable( function*(func){ var i = 0 try{ for(var e of this){ if(func(e, i++, this)){ yield e } } // normalize the stop value... } catch(err){ if(err instanceof STOP){ if(!err.value){ throw STOP } err.value = e } throw err } }, __onstop), sfilter: function*(func){ yield* this.filter(...arguments) }, reduce: stoppable( function(func, res){ var i = 0 for(var e of this){ res = func(res, e, i++, this) } return res }, // NOTE: we need to wrap __onstop(..) here to prevent res if it // was passed a function from ever being treated as onstop(..)... function(res, f, _, onstop){ return __onstop.call(this, res, onstop) }), sreduce: function*(func, res){ yield* this.reduce(...arguments) }, greduce: function*(func, res){ yield this.reduce(...arguments) }, between: stoppable( function*(func){ var i = 0 var j = 0 var prev for(var e of this){ if(i > 0){ yield typeof(func) == 'function' ? func.call(this, [prev, e], i-1, i + j++, this) : func } prev = e yield e i++ } }, __onstop), // NOTE: this is a special case in that it will unwind the generator... // NOTE: this is different from .forEach(..) in that this will // return the resulting array. // XXX EXPERIMENTAL forEach: function(func){ return [...this].map(func) }, sforEach: function(func){ return this.forEach(func) }, pop: function(){ return [...this].pop() }, // XXX this needs the value to be iterable... gpop: function*(){ yield [...this].pop() }, push: function*(value){ yield* this yield value }, shift: function(){ return this.next().value }, // XXX this needs the value to be iterable... gshift: function*(){ yield this.next().value }, unshift: function*(value){ yield value yield* this }, // non-generators... // toArray: function(){ return [...this] }, // promises... // // NOTE: this will unwind the generator... unwind: function(onresolve, onreject){ var that = this var p = new Promise( function(resolve){ resolve([...that]) }) p = (onresolve || onreject) ? p.then(...arguments) : p return p }, then: function(...args){ return this.unwind(...args) }, catch: function(func){ return this.unwind().catch(...arguments) }, finally: function(func){ return this.unwind.finally(...arguments) }, // combinators... // chain: function*(...next){ yield* next .reduce(function(cur, next){ return next(cur) }, this) }, concat: function*(...next){ yield* this for(var e of next){ yield* e } }, // XXX EXPERIMENTAL... /* XXX not sure how to do this yet... tee: function*(...next){ // XXX take the output of the current generator and feed it into // each of the next generators... (???) }, zip: function*(...items){ // XXX }, //*/ enumerate: function*(){ var i = 0 for(var e of this){ yield [i++, e] } }, join: function(){ return [...this] .join(...arguments) }, }) GeneratorMixin(Generator.prototype) GeneratorProtoMixin(Generator.prototype.prototype) // Extend base iterators... ITERATOR_PROTOTYPES .forEach(function(proto){ GeneratorProtoMixin(proto) }) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // XXX EXPERIMENTAL... var AsyncGeneratorMixin = module.AsyncGeneratorMixin = object.Mixin('AsyncGeneratorMixin', 'soft', { // XXX TEST... iter: makeGenerator('async', 'iter'), map: makeGenerator('async', 'map'), filter: makeGenerator('async', 'filter'), reduce: makeGenerator('async', 'reduce'), // XXX TEST... between: makeGenerator('async', 'between'), }) var AsyncGeneratorProtoMixin = module.AsyncGeneratorProtoMixin = object.Mixin('AsyncGeneratorProtoMixin', 'soft', { // promise... // // NOTE: this will unwind the generator... // XXX create an iterator promise??? unwind: function(onresolve, onreject){ var that = this var p = new Promise(async function(resolve){ var res = [] for await(var elem of that){ res.push(elem) } resolve(res) }) p = (onresolve || onreject) ? p.then(...arguments) : p return p }, then: function(...args){ return this.unwind(...args) }, catch: function(func){ return this.unwind().catch(...arguments) }, finally: function(func){ return this.unwind.finally(...arguments) }, // XXX might be a good idea to use this approach above... iter: stoppable( async function*(handler=undefined, onerror=undefined){ try{ var i = 0 if(handler){ for await(var e of this){ var res = handler.call(this, e, i++) if(typeof(res) == 'object' && Symbol.iterator in res){ yield* res } else { yield res } } } else { yield* this } }catch(err){ if(onerror){ if(!(err === STOP || err instanceof STOP)){ var res = onerror(err) if(res !== undefined){ yield handler ? handler(res) : res } return } } throw err } }, __onstop), map: async function*(func){ yield* this.iter(function(elem, i){ return [func.call(this, elem, i)] }) }, filter: async function*(func){ yield* this.iter(function(elem, i){ return func.call(this, elem, i) ? [elem] : [] }) }, // NOTE: there is not much point in .reduceRight(..) in an async // generator as we'll need to fully unwind it then go from the // end... reduce: async function(func, state){ this.iter(function(elem, i){ state = func.call(this, state, elem, i) return [] }) return state }, // XXX BETWEEN... between: async function*(func){ var i = 0 var j = 0 var prev yield* this.iter(function(e){ return i++ > 0 ? [ typeof(func) == 'function' ? func.call(this, [prev, e], i, i + j++, this) : func, e, ] : [e] }) }, // XXX TEST... chain: async function*(...next){ yield* next .reduce(function(cur, next){ return next(cur) }, this) }, flat: async function*(){ for await(var e of this){ if(e instanceof Array){ yield* e } else { yield e }}}, concat: async function*(other){ yield* this yield* other }, push: async function*(elem){ yield* this yield elem }, unsift: async function*(elem){ yield elem yield* this }, join: async function(){ return [...(await this)] .join(...arguments) }, // XXX // slice -- not sure if we need this... // ... }) AsyncGeneratorMixin(AsyncGenerator.prototype) AsyncGeneratorProtoMixin(AsyncGenerator.prototype.prototype) //--------------------------------------------------------------------- // Generators... // NOTE: step can be 0, this will repeat the first element infinitely... var range = module.range = function*(from, to, step){ if(to == null){ to = from from = 0 } step = step ?? (from > to ? -1 : 1) while(step > 0 ? from < to : from > to){ yield from from += step } } var repeat = module.repeat = function*(value=true, stop){ while( typeof(stop) == 'function' && stop(value) ){ yield value } } var produce = module.produce = stoppable( function*(func){ while(true){ yield func() } }, __onstop) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })