Iterable protocol and observable

I’m trying to create a custom iterable, and I’m trying to return it in a way that Observable recognises it, but I cannot figure out how to make it work. Writing this little wrapper works, but I’d like Observable to recognise the object returned from customIterable as an Iterable automatically, is that possible?

{
  const customIterable = () => ({
    [Symbol.asyncIterator]: function() {
      let i = 0
      return {

        next: function() {
          i++
          return delay(1).then(() => ({
            value: i,
            done: false
          }))
        }
      }
    }
  })
  
  const iterator = customIterable()[Symbol.asyncIterator]()
  while (true) {
    const {done, value} = await iterator.next();
    if (done) return;
    yield value;
  }
  
}

It seeeeems like it should be possible, because if I do this:

async function* t() {
  await delay(1)
  yield 1
  await delay(1)
  yield 2
  await delay(1)
  yield 3
}

and then just call

t()

From a cell, it properly iterates the values. What is the difference between what t() returns and what customIterable() returns that make Observable recognise the other?

You need to return a “generatorish” object rather than an iterable; specifically, Observable looks for the generator.next and generator.return functions to test whether a cell is a generator. Here’s a new notebook on custom generators and iterables:

1 Like

Thank you, thats solves my problem!

Out of curiosity, why does it need to be generatorish and not just an iterator? What is the return method used for?

If we interpreted any iterable as a value that changes over time (rather than restricting this interpretation to generators), there would be many false positives: cases where you intended the value of the cell to be an iterable object, but instead Observable pulled values iteratively. In particular, Array, Map, Set and other common collections in JavaScript are iterable. So if we allowed any iterable and you said:

x = [0, 1, 2, 3, 4, 5, …]

The value of x as seen from other cells would not be the array; it’d first be 0, then 1, then 2, sixty times a second. It would be as if there were an implicit yield-star:

x = yield* [0, 1, 2, 3, 4, 5, …]

Generators are less common that iterables, so it was convenient to limit the special interpretation to this more specific type. Also, you can easily adapt any iterable to a generator through yield-star, providing an opt-in interpretation if desired.

Also, like an iterator but not necessarily an iterable, reading a generator is inherently destructive (read-once). Multiple cells reading directly from a single generator using generator.next would be non-deterministic, and you generally want to avoid side-effects in Observable. Making generator the representation of a value over time affords more deterministic behavior.

Note that reading an iterator is also destructive, and even an iterable is sometimes: many iterators implement iterable (the [Symbol.iterator] method) by returning itself! For example, a Map can be iterated many times, but the return value of map.values can only be iterated once; you have to call map.values each time you want to iterate. But the common types are pure, and you can always splat a destructive iterable like map.values into an array using the spread operator:

[...map.values()]

To answer the other question, generator.return allows generators (and generator cells) to clean up after themselves. You can read about that here:

Also, in case anyone reading hasn’t seen it yet, here is the introduction to generators:

3 Likes