mirror of
https://github.com/flynx/types.js.git
synced 2025-10-28 10:00:08 +00:00
743 lines
18 KiB
JavaScript
743 lines
18 KiB
JavaScript
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************/ /* 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(<name>)
|
|
// makeGenerator(<name>, <handler>)
|
|
// -> <func>
|
|
//
|
|
// makeGenerator('async', <name>)
|
|
// makeGenerator('async', <name>, <handler>)
|
|
// -> <func>
|
|
//
|
|
//
|
|
// <func>(...args)
|
|
// -> <Generator>
|
|
//
|
|
// <Generator>(...inputs)
|
|
// -> <generator>
|
|
//
|
|
// <handler>(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 <array>.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 })
|