Coder Social home page Coder Social logo

proposal-iterator-helpers's Introduction

Iterator Helpers

A proposal for several interfaces that will help with general usage and consumption of iterators in ECMAScript.

Status

Authors: Gus Caplan, Michael Ficarra, Adam Vandolder, Jason Orendorff, Kevin Gibbons

Champions: Michael Ficarra, Yulia Startsev

This proposal is at Stage 3 of The TC39 Process.

This proposal formerly contained async as well as sync helpers. The async helpers have been split out to a separate proposal.

Motivation

Iterators are a useful way to represent large or possibly infinite enumerable data sets. However, they lack helpers which make them as easy to use as Arrays and other finite data structures, which results in certain problems that could be better represented by iterators being expressed in Arrays, or using libraries to introduce the necessary helpers. Many libraries and languages already provide these interfaces.

Proposal

The proposal introduces a collection of new methods on the Iterator prototype, to allow general usage and consumption of iterators. For specifics on the implemented methods, please refer to the specification.

See DETAILS.md for details on semantics decisions.

See this proposal rendered here

Added Methods

For Iterators we add the following methods:

.map(mapperFn)

map takes a function as an argument. It allows users to apply a function to every element returned from an iterator.

Returns an iterator of the values with the map function applied.

Example

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const result = naturals()
  .map(value => {
    return value * value;
  });
result.next(); //  {value: 0, done: false};
result.next(); //  {value: 1, done: false};
result.next(); //  {value: 4, done: false};

.filter(filtererFn)

filter takes a function as an argument. It allows users to skip values from an iterator which do not pass a filter function.

Returns an iterator of values from the original iterator that pass the filter.

Example

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const result = naturals()
  .filter(value => {
    return value % 2 == 0;
  });
result.next(); //  {value: 0, done: false};
result.next(); //  {value: 2, done: false};
result.next(); //  {value: 4, done: false};

.take(limit)

take takes an integer as an argument. It returns an iterator that produces, at most, the given number of elements produced by the underlying iterator.

Returns an iterator with items from the original iterator from 0 until the limit.

Example

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const result = naturals()
  .take(3);
result.next(); //  {value: 0, done: false};
result.next(); //  {value: 1, done: false};
result.next(); //  {value: 2, done: false};
result.next(); //  {value: undefined, done: true};

.drop(limit)

drop takes an integer as an argument. It skips the given number of elements produced by the underlying iterator before itself producing any remaining elements.

Returns an iterator of items after the limit.

Example

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const result = naturals()
  .drop(3);
result.next(); //  {value: 3, done: false};
result.next(); //  {value: 4, done: false};
result.next(); //  {value: 5, done: false};

.flatMap(mapperFn)

.flatMap takes a mapping function as an argument. It returns an iterator that produces all elements of the iterators produced by applying the mapping function to the elements produced by the underlying iterator.

Returns an iterator of flat values.

Example

const sunny = ["It's Sunny in", "", "California"].values();

const result = sunny
  .flatMap(value => value.split(" ").values());
result.next(); //  {value: "It's", done: false};
result.next(); //  {value: "Sunny", done: false};
result.next(); //  {value: "in", done: false};
result.next(); //  {value: "", done: false};
result.next(); //  {value: "California", done: false};
result.next(); //  {value: undefined, done: true};

.reduce(reducer [, initialValue ])

reduce takes a function and an optional initial value as an argument. It allows users to apply a function to every element returned from an iterator, while keeping track of the most recent result of the reducer (the memo). For the first element, the given initial value is used as the memo.

Returns a value (in the example, a number) of the type returned to the reducer function.

Example

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const result = naturals()
  .take(5)
  .reduce((sum, value) => {
    return sum + value;
  }, 3);

result // 13

.toArray()

When you have a non-infinite iterator which you wish to transform into an array, you can do so with the builtin toArray method.

Returns an Array containing the values from the iterator.

Example

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const result = naturals()
  .take(5)
  .toArray();

result // [0, 1, 2, 3, 4]

.forEach(fn)

For using side effects with an iterator, you can use the .forEach builtin method, which takes as an argument a function.

Returns undefined.

Example

const log = [];
const fn = (value) => log.push(value);
const iter = [1, 2, 3].values();

iter.forEach(fn);
console.log(log.join(", ")) // "1, 2, 3"

.some(fn)

To check if any value in the iterator matches a given predicate, .some can be used. It takes as an argument a function which returns true or false.

Returns a boolean which is true if any element returned true when fn was called on it. The iterator is consumed when some is called.

Example

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const iter = naturals().take(4);

iter.some(v => v > 1); // true
iter.some(v => true); // false, iterator is already consumed.

naturals().take(4).some(v => v > 1); // true
naturals().take(4).some(v => v == 1); // true, acting on a new iterator

.every(fn)

.every takes a function which returns a boolean as an argument. It is used to check if every value generated by the iterator passes the test function.

Returns a boolean.

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

const iter = naturals().take(10);

iter.every(v => v >= 0); // true
iter.every(v => false); // true, iterator is already consumed.

naturals().take(4).every(v => v > 0); // false, first value is 0
naturals().take(4).every(v => v >= 0); // true, acting on a new iterator

.find(fn)

.find takes a function as an argument. It is used to find the first element in an iterator that matches.

Can be used without take on infinite iterators.

Returns the found element, or undefined if no element matches fn.

function* naturals() {
  let i = 0;
  while (true) {
    yield i;
    i += 1;
  }
}

naturals().find(v => v > 1); // 2

Iterator.from(object)

.from is a static method (unlike the others listed above) which takes an object as an argument. This method allows wrapping "iterator-like" objects with an iterator.

Returns the object if it is already an iterator, returns a wrapping iterator if the passed object implements a callable @@iterator property.

class Iter {
  next() {
    return { done: false, value: 1 };
  }
}

const iter = new Iter();
const wrapper = Iterator.from(iter);

wrapper.next() // { value: 1, done: false }

Iterator helpers and the generator protocol

The generator protocol facilitates coordination between a producer and a consumer, which is necessarily broken by iteration-based transforms. There is no way to properly preserve or re-establish this coordination. We've taken the philosophy that any iterators produced by the helpers this proposal adds only implement the iterator protocol and make no attempt to support generators which use the remainder of the generator protocol. Specifically, such iterators do not implement .throw and do not forward the parameter of .next or .return to an underlying or "source" iterator.

Extending Iterator Prototype

With this proposal, it will be easier to extend the IteratorPrototype for a custom class. See the below example for the previous implementation compared to the new one.

const MyIteratorPrototype = {
  next() {},
  throw() {},
  return() {},

  // but we don't properly implement %IteratorPrototype%!!!
};

// Previously...
// Object.setPrototypeOf(MyIteratorPrototype,
//   Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())));

Object.setPrototypeOf(MyIteratorPrototype, Iterator.prototype);

Implementations

Implementation tracking of Iterator Helpers

  • Browsers:
    • V8
    • SpiderMonkey (feature-flagged on Nightly only)
    • JavaScriptCore

Q & A

Why not use Array.from + Array.prototype methods?

All of the iterator-producing methods in this proposal are lazy. They will only consume the iterator when they need the next item from it. Especially for iterators that never end, this is key. Without generic support for any form of iterator, different iterators have to be handled differently.

How can I access the new intrinsics?

const IteratorHelperPrototype = Object.getPrototypeOf(Iterator.from([]).take(0));
const WrapForValidIteratorPrototype = Object.getPrototypeOf(Iterator.from({ next(){} }));

Prior Art & Userland implementations

Method Rust Python npm Itertools C#
all
any
chain
collect
count
cycle
enumerate
filter
filterMap
find
findMap
flatMap
flatten
forEach
last
map
max
min
nth
partition
peekable
position
product
reverse
scan
skip
skipWhile
stepBy
sum
take
takeWhile
unzip
zip
compress
permutations
repeat
slice
starmap
tee
compact
contains
range
reduce
sorted
unique
average
empty
except
intersect
prepend
append

Note: The method names are combined, such as toArray and collect.

proposal-iterator-helpers's People

Contributors

2767mr avatar alexendoo avatar arai-a avatar avandolder avatar bakkot avatar benjamingr avatar btoo avatar codehag avatar decompil3d avatar devsnek avatar exe-boss avatar gowee avatar graingert avatar karlhorky avatar kt3k avatar lightmare avatar ljharb avatar michaelficarra avatar mrbrianevans avatar notwoods avatar rauschma avatar robey avatar tniessen avatar trotyl avatar zloirock avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-iterator-helpers's Issues

Iterator namespace as adaptor function

Some years ago was created, but not presented to TC39 an alternative iterators HOF proposal.

The main difference is that Iterator is a function which returns adopted iterator if passed iterator is not inherited from %IteratorPrototype%. It looks more useful than just namespaces. Maybe makes sense make Iterator and AsyncIterator functions-adapters?

.reduce without initial argument

For consistency with other .reduce methods, Iterator.prototype.reduce should allow work without initial argument. But it does not work by the current spec.

Iterator.from([1, 2, 3, 4, 5]).reduce((a, b) => a + b) // => NaN

Non-existent adoption/usage in the wild

Whilst I think there is some value in doing this, it's a little bit concerning that usage of this approach of having transforms as iterators is effectively zero (itertools). To quote @domenic, do we need to get high adoption first? If not, at least explore/reflect on why the wider community has explored and decided not to take this approach?:

I think a crucial part of getting stage 2 (i.e., commitment that this should be in the language) is signs that this method gets high adoption, comparable to existing libraries, and that TC39 isn't pushing through something that the community is uninterested in. So I think it should be released during stage 1, and high adoption for the library will be a stage 2 prerequisite.

`Iterator` should be a class

Currently, Iterator is just a {prototype, from} object. I feel it should instead be a class with those methods:

class Iterator {
	constructor() {}
	// `static from` + other methods
}

That way, custom iterators can easily be written to either use it directly or just subclass it and implement what they want.

Should map()/filter()/reduce() have a thisArg?

Personally I would really like it if the answer was "no". That's a remnant of the pre-.bind() era, as far as I'm concerned.

But, consistency would demand "yes". A classic consistency vs. goodness tradeoff.

Something to discuss!

Research the cross-section of all possible methods

https://github.com/devsnek/proposal-iterator-helpers#prior-art is a great start. I think it would help crystalize discussions if you had a table or spreadsheet that had a method per row, and a column for each language/library, and Xs for if a method was present. Also probably good to have a column for the built-in Array.prototype.

I still think it's a very solid strategy to begin with only map/filter/reduce, and will prefer to start that way. But I think people will want to have the discussion about expanding the scope soon, and setting the stage with a nice table will make the discussion more data-driven and less intuition-driven.

AsyncIterator.from and sync iterables

AsyncIterator.from([1, 2, 3]).toArray().then(console.log); // Error

since the current logic works as

if (typeof O[Symbol.asyncIterator] == 'function') {
  // get iterator
} else {
  // it's already iterator
}

Seems it also should convert sync iterables to async.

How would these be inherited by custom iterators?

Some iterators will inevitably be specified in userland, and this proposal makes inheriting from %IteratorPrototype% even more valuable. Will there be any standard way to expose this to userland code, whether it be a new Iterator({next?, throw?, return?}) global (with methods delegating to .next(value?), .throw(error), and .return(value?)), an Object.iteratorPrototype class, or similar?

`HasProperty` in `Iterator.from`

Here we have something like

if (Symbol.iterator in O) {
  // work with iterables
} else {
  // work with non-iterables
}

in other cases, like Array.from or %TypedArray%.from, something like:

const usingIterator = O[Symbol.iterator];
if (typeof usingIterator == 'function') {
  // work with iterables
} else if (usingIterator == null) {
  // work with non-iterables
} else {
  throw TypeError();
}

Why?

Suggested tweaks to API documentation

  • Remove the * and async prefixes; those are just confusing IMO.

  • Add a brief description, at least for collect() and of() since those are non-obvious.

Forkable generators

I've got a related proposal that suggests forkable generators. I do specifically avoid having it generalized, because the behavior could be anywhere from unexpected to just plain wrong, and generator callees are not always written to potentially support cloning and replaying continuations with different next parameters.

The "Potential FAQs" section was written in part as a response to initial #tc39 IRC feedback, and I'm filing this issue based on a recommendation there.

.from and iterables with custom iterators

class CustomIterable {
  constructor(end) {
    this.end = end;
  }
  [Symbol.iterator]() {
    let counter = 0;
    return {
      next: () => {
        return { value: counter++, done: counter > this.end }
      }
    }
  }
}

[...new CustomIterable(10]; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// new CustomIterable(10) is a correct iterable by the current iterators protocol,
// but it will not work by the current spec draft since iterator is not inherited
// from %IteratorPrototype%:
Iterator.from(new CustomIterable(10)).forEach(console.log);

range doesn't belong in this proposal

Merits of range aside, it is different from the other functions which both consume and produce iterators. I think that it should be a separate proposal, and this proposal should just focus on common transformations.

Override @@hasInstance

Something we could do is override @@hasInstance on Iterator to allow x instanceof Iterator, but should we? Assumedly @@hasInstance exists for situations like this.

what is the appropriate minimal set of combinators?

#4 did a great job making data about common combinators available for our review. And it looks like this proposal has done a good job of staying very minimal. But I feel that if we thought it was okay to expand the minimal set a bit, I have found the following combinators to be most useful, in order:

  1. flatMap (we just added it to Array for a reason)
  2. zip (present across all of the analysed libraries)
  3. scan

Additionally, it is fair to include the more general -While or -With variants of these combinators:

  1. zipWith
  2. takeWhile
  3. dropWhile

take without drop is very surprising

We don't have to have take, but if we do, it should coincide with drop. As a user, I would be very surprised to be able to use one and not the other.

Consider removing %WrapForValidIteratorPrototype%

This was introduced in 49d13bd, and it wasn't clear to me what the rationale is, so maybe I'm just missing something.

I propose changing Iterator.from to do the first five steps of GetIterator and then return iterator.

Rationale:

  • The iterable protocol is already standard. A method called Iterator.from should implement that protocol, not introduce extra visible objects.

  • It seems like this might be designed for use as an adapter for existing iterator objects that for some reason don't inherit from %IteratorPrototype%. Assuming there are libraries or something that create such iterators, they should just consider switching to inherit from %IteratorPrototype%! It's easy to do and highly backward-compatible. Even if they won't, end users faced with such objects can use a workaround like function *toStandardIterator(iter) { return yield* iter; }—my feeling here is that maybe not every workaround for a stubbornly broken library needs to be incorporated in the standard.

@@toStringTag on wrappers

Should iterators wrappes have @@toStringTag? Since now:

Iterator.from({
  next: () => ({ done: Math.random() > .9, value: Math.random() * 10 | 0 })
}).toString() // => [object Object]

reviewing

cc @michaelficarra @dtribble

this isn't quite ready for stage 3 yet (i need to review some error behaviour), but i wanted to let y'all know pretty much all the semantics are laid out at this point. if you have any insights/criticism/etc please feel free to share them and i will try to address them when i can (i'm starting university over the next few weeks so things will be busy).

generalised collect (transducers)

There is a pattern in imperative programming languages for composing transformations on iterable data structures called "transducing". In this pattern, we split apart the three concepts of iteration, transformation, and accumulation. For a good intro to this concept (applied to JS, even!), I highly recommend viewing the ForwardJS 2016 lecture A million ways to fold in JS by Brian Lonsdorf (@DrBoolean). A concrete benefit of this pattern is the ability to fuse the transformation operations (avoiding multiple iteration). The good news is that we basically follow this pattern already. But I would like our built-in accumulation to be fully general.

Concretely, I propose we parameterise collect with an "empty" value and a "combining" operation (or an object that provides them). Precedent for this more generalised accumulation exists in Java's Stream where collect is passed a Collector. Java also has a whole bunch of built-in accumulators and generators of accumulators in its Collectors class, which we could also provide in the future.

Alternatively, if we reject this generalisation, I propose we rename collect to toArray. That would make me sad.

Validation of `.next` method on iterators

Iterator methods does not validate .next methods of this:

Iterator.prototype.drop.call({}) // => no error before the first `.next` call

Seems this validation should be added since those methods should work by iterators protocol and use duck typing.

Combine nested iterators (a sort of flatMap)

Those 2 expressions give the same result, but only the first one can iterate lazily

[...tripleSorted(1, 10)];

[...range(1, 10)]
  .flatMap(a =>   [...range(a+1, 10)]
    .flatMap(b => [...range(b+1, 10)]
      .flatMap(c => [[a, b, c]])
    )
  );

// where range and tripleSorted are:
function* range(si, ei) {
  for (let i = si; i < ei; i += 1) yield i;
}

function* tripleSorted(start, end) {
  for (const a of range(1, 10)) {
    for (const b of range(a+1, 10)) {
      for (const c of range(b+1, 10)) {
        yield [a, b, c];
      }
    }
  }
}

Would it possible, with this proposal, or with an additional iterator method, to combine those 3 range() iterators lazily and behave like tripleSorted?

Edit: some code achieving the same result lazily

function range(si, ei) {
  return ({
    *[Symbol.iterator]() {
      for (let i = si; i < ei; i += 1) yield i;
    },
    *flatMap(fn) {
      for (const o of this) yield* fn(o);
    },
    *map(fn) {
      for (const o of this) yield fn(o);
    }
  })
}

const tripleIt = range(1,10)
  .flatMap(a => range(a+1,10)
    .flatMap(b => range(b+1,10)
      .map(c => [a, b, c])));
[...tripleIt]

the idea is to be able to express something equivalent to python's list comprehensions (even if it's not necessarily lazy in that case):

 [(a,b,c) for a in range(1,10)
          for b in range(a+1,10)
          for c in range(b+1,10)]

Possible intersection with protocol proposal

Exposing Iterator.prototype and Iterator.asyncPrototype may intersect with the design of https://github.com/michaelficarra/proposal-first-class-protocols.

I assume if protocols happen, %IteratorPrototype% would exist only to implement an Iterator protocol, and creating your own manual iterator would implement Iterator instead of having a prototype of %IteratorPrototype%.

const myAmazingIterator = { next() {} };

Object.setPrototypeOf(myAmazingIterator, Iterator.prototype);
// or
Protocol.implement(myAmazingIterator, IteratorProtocolFromSomewhere);

cc @michaelficarra

Should we consider an FP-style in light of pipeline

While it may take some time to smooth out the pipeline proposal (and related proposals), it seems like this would be a natural feature for functional programming. Should we consider having an FP-style implementation of these helpers in parallel to, or in place of, this effort?

The main issue with using a prototype-based approach to this would be any attempt to mix this with |> when using 3rd-party FP iteration helpers:

map.keys()
  .filter()
  .map(...)
  |> spanMap(?, )
  // .filter(…) - oops, need to wrap the whole expression in parens.

iterators vs generators

The current spec draft uses as wrappers for iterators generators in instance methods and iterators in .from methods. IIRC the rest part of the ES spec does not use generators in the standard library. Sure, generators semantic is simpler for the writing of the spec text. But why this inconsistency?

AsyncIterator.from spec bugs

  • It will not work with async iterables, just with iterables

Let usingIterator be ? GetMethod(O, @@iterator)

  • It will wrap async iterators, since they are not %IteratorPrototype% instances

Let hasInstance be ? OrdinaryHasInstance(%Iterator.prototype%, iteratorRecord.[[Iterator]])

Iterator.prototype.to and a new protocol

It could look like a new proposal, but since this proposal includes Iterator.prototype.toArray method, seems, it should be opened here and, maybe, be a part of this proposal.

So, this proposal contains .toArray, by why it's limited only to arrays? Instead of adding methods for each collections type (.toObject, .toSet, etc.), we could add one: .to(Collection) and a protocol for that.

Instead of

Object.fromEntries([1, 2, 3, 4, 5]
  .map(it => it ** 2)
  .filter(it => it % 2)
  .values()
  .map(it => [`key${it}`, it]))
// => { key0: 1, key1: 9, key2: 25 }

we could use a pipeline operator:

[1, 2, 3, 4, 5]
  .map(it => it ** 2)
  .filter(it => it % 2)
  .values()
  .map(it => [`key${it}`, it])
  |> Object.fromEntries
// => { key0: 1, key1: 9, key2: 25 }

But it's mixin of different operators and readable not very good.

In my opinion, the chaining of methods could be better:

[1, 2, 3, 4, 5]
  .map(it => it ** 2)
  .filter(it => it % 2)
  .values()
  .map(it => [`key${it}`, it])
  .to(Object)
// => { key0: 1, key1: 9, key2: 25 }

Adding .toArray to this proposal is adding the same, but only for arrays, more other, we have an additional simple way of conversion to arrays - spread operator.

So, how should work .to method? It's just passing this to the method of the passed constructor, the name of this method is a part of this protocol. So, what could be used as this protocol?

  • It could be .from method.

At this moment, it's available of Array, %TypedArray%, on Observable proposal, available proposal for adding .from method to Set, Map, WeakSet, WeakMap.
But it missed at least on Object (here used .fromEntries) and URLSearchParams.

  • It could be .fromIterable method or @@fromIterable well-known symbol.

More other, since here used iterables protocol, it could be added not only to Iterator.prototype but also to prototypes of all iterables, but it's not a part of this proposal...

[1, 2, 3, 2, 1].to(Set).map(it => [`key${it}`, it]).to(Map);

So, something like that:

Symbol.fromIterable = Symbol('Symbol.fromIterable');
Symbol.fromAsyncIterable = Symbol('Symbol.fromAsyncIterable');
const TypedArray = Object.getPrototypeOf(Int8Array);

for (const C of [Array, TypedArray, Map, Set, URLSearchParams, Iterator]) {
  C.prototype.to = function to(Constructor) {
    return Constructor[Symbol.fromIterable](this);
  };
}

AsyncIterator.prototype.to = async function to(Constructor) {
  if (Constructor[Symbol.fromAsyncIterable]) {
    return Constructor[Symbol.fromAsyncIterable](this);
  } return Constructor[Symbol.fromIterable](await AsyncIterator.from(this).toArray());
};

Object[Symbol.fromIterable] = function (iterable) {
  return this.fromEntries(iterable);
};

for (const C of [Array, TypedArray, Iterator, AsyncIterator]) {
  C[Symbol.fromIterable] = C.from;
}

for (const C of [Map, Set, WeakMap, WeakSet, URLSearchParams]) {
  C[Symbol.fromIterable] = function (iterable) {
    return new this(iterable);
  };
}

AsyncIterator[Symbol.fromAsyncIterable] = AsyncIterator.from;

Feedback from last attempt at this proposal:

During a call with @littledan today, I refreshed my knowledge of a bunch of the open proposals and am particularly excited to see this one!

I wanted to offer feedback based on my attempt at introducing this in TC39 ~4 years ago

https://github.com/leebyron/ecmascript-iterator-hof

  • A primary stumbling block I reached was reverse, since it was confusing with Array.prototype.reverse which is mutable. I suggest reversed to disambiguate. This would be an eager method, but it's a really common one that I got feedback was worth including.

  • My proposal came before AsyncIterators were approved, so I'm really happy to see them represented here. A small note, I think collect is missing from your spec text.

  • I really encourage some way to include an equivalent to tee from python's itertools. It's widely used in Python iterator comprehensions and allows reusing/memoizing more complex chains of iterator transforms.

  • I also really encourage an equivalent to zip, however I'd not suggest the variant from my older proposal. After learning from other libraries and feedback against Immutable.js, I'd suggest a static function on the Iterator global, so Iterator.zip(x, y) could take any number of Iterables and yield an Iterator of tuples of the size of the number of arguments. There's also a variant of zip not captured in your prior art worth capturing, sometimes called zipAll - zip usually yields its last value when any of the inputs complete, zipAll continues with undefined in each slot until all inputs have completed.

  • Another worthwhile prior art: https://clojure.org/reference/sequences

Consider making the library for Iterables rather than Iterators

Generally when working with iterables it is nicer to be able to use the result of applying combinators multiple times if the source iterable is re-usable.

For example suppose we have a custom combinator repeat:

function* repeat(iterable, times) {
  for (let i = 0; i < times; i++) {
    yield* iterable
  }
}

// Using the proposed spec's Iterator.map

const seq = Iterator.of(1,2,3)
  .map(x => x**2)

repeat(seq, 2).collect() // [1,4,9] Which isn't really expected

The solution to this is to wrap everything in a [Symbol.iterator]() method:

function repeat(iterable, times) {
  // Where iterable.from creates an object that wraps the
  // Iterable.prototype correctly
  return Iterable.from({
    * [Symbol.iterator]() {
      for (let i = 0; i < times; i++) {
        yield* iterable
      }
    }
  })
}

// Iterable.prototype.map = function map(mapperFn) {
//   return Iterable.from({
//     * [Symbol.iterator]() {
//       for (const item of this) {
//         yield mapperFn(item)
//       }
//     }
//   })
// }

const seq =  Iterable.of(1,2,3)
  .map(x => x**2)

repeat(seq, 2).collect() // [1, 4, 9, 1, 4, 9] as expected

However this is quite tedious, I actually implemented this pattern in my own library and it prevents a lot of footguns especially when implementing more specialised operators.

More suggested examples

  • Use some of the iterators in the spec already, e.g. an example that operates on map.entries() or similar.

  • Show how .collect() works to give a nice array which you can do array-ish things on, like supply to a UI framework that only takes arrays.

  • Show how .collect() works on async iterables to eagerly drain the async iterable. This can be a bit of a footgun but is quite useful some times; perhaps link to the discussion at WICG/kv-storage#6 (comment)

`.take` and fractional numbers

It should not use just ToNumber conversion or is 0 check, since:

Iterator.from([1, 2, 3, 4]).take(2.5).toArray() // => [1, 2, 3, 4]

.concat in version 1, maybe ?

Should the (sync) Iterator have a concat method as well ?

Rationale:

  • .startWith / .endWith are trivially imlementable with .concat
  • connecting several streams iterators together seems like a normal task to do
  • spreading into array just to be able to concat seems like a waste.

Async:

  • i personally think concat on async iterators also makes sense, but from Rx experience you want it less often than in sync world. (And rxjs-style merge is out of scope of this proposal).

Compare with adding individual methods to every collection

A natural question to ask is, when should we add things to Iterator.prototype vs. to each of Array.prototype, Set.prototype, Map.prototype, etc.?

The answer is that we should do both. Each serve different use cases:

  • On-collection methods allow optimized-for-each-collection implementations, that are more convenient to use in the common case. For example, a set.map(x => y) would return a Set. This is simpler than indirecting through new Set(set.values().map(x => y)), and semantically it is always eager, which is sometimes what you want.
  • Iterator.prototype methods provide a nice way to manipulate the .values(), .entries(), and .keys() of an individual collection. They're always lazy, which is sometimes what you want.

Maybe link to the Set methods proposal to show how they coexist nicely.

That's my brain dump, but I'm sure it can be expanded.

Consider making flatMap throw if the mapper returns a non-iterable

In Array.prototype.flatMap, if the mapping function returns a non-array, it is treated as if it returned an array of length 1. That was to preserve symmetry with .flat.

Here, I think it makes sense to be more strict (that is, to throw), for three reasons:

  1. There's no .flat to keep parity with.
  2. This version of .flatMap flattens all iterables, not just arrays. In particular, it flattens strings. I think anyone relying on the auto-boxing behavior would find that surprising, so I think we should just not have the auto-boxing behavior.
  3. Auto-boxing would mean that adding a Symbol.iterator method to any object which did not previous have it would be a breaking change.

'collect' should 'toArray'

ECMAScript already has .toString() and .toJSON(), so it seems like this should be .toArray(). The explainer indicates that C#/.NET does not have a .collect() like behavior, but it does in the form of Enumerable.ToArray. The to prefix also indicates a conversion/transition from the iterator chain into a materialized value (in which the iteration is immediately performed), similar to how .toString() and .toJSON() indicate a conversion.

I'd eventually like to be able to turn a chained iterator into a Set or a Map as well, so a later toSet or toMap would align with this naming.

Merge with Standard Library proposal?

This proposal doesn't introduce any new mechanic that needs changes to be made to the core language that otherwise we can't really already do, so I think adding one more thing to the global namespace is unnecessary. Our stdlib is also in stage 1 already.

import { Iterator } from "std:Iterator";
// all the rest of the fun things

Iterator.from and async

Right now this proposal defines Iterator.from.

Should we have:

  1. Iterator.fromAsync?
  2. SyncIterator.from and AsyncIterator.from?
  3. Something else?

Keep in mind that we also have Iterator.syncPrototype and Iterator.asyncPrototype, and I assume we want to avoid using the literal name "prototype".

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.