Philipp Gfeller
Philipp Gfeller

Reputation: 1259

Get current value from an iterator

I was looking into javascript generators and iterators and was wondering if there is a way to write a generator function to return the value at the current position --- without of course having to call next() or to remember the returned value from the last next() call.

More specific, my failed attempt:

function* iterable(arr) {
  this.index = 0;
  this.arr = arr;
  while(this.index < this.arr.length) {
    yield this.arr[this.index++];
  }
}
iterable.prototype.current = function () {
  return this.arr[this.index];
}

const i = iterable([0, 1, 2]);
console.log(i.current()); // TypeError: Cannot read property 'undefined' of undefined

The desired functionality could be implemented using a class like this (I'm aware of the fact that the return values from the iterator would be objects like { value: 1, done: false }):

class iterableClass {
  constructor(arr) {
    this.index = 0;
    this.arr = arr;
  }
  get(i) {
    return this.index < arr.length ? this.arr[this.index] : false;
  }
  next() {
    const val = this.get(this.index);
    this.index++;
    return val;
  }
  current() {
    return this.get(this.index);
  }
}
const i = iterableClass([0, 1, 2]);
console.log(i.current()); // 0

While I could just work with the class (or even a plain old function), I was wondering if this could be done with a generator/iterator or maybe there's an even better option.

Upvotes: 0

Views: 5047

Answers (3)

Bergi
Bergi

Reputation: 664579

The problem with your generator function is that a) it doesn't start running when you call it, it just creates the generator (this.arr and this.index won't be initialised until the first call to next()) and b) there is no way to access the generator object from inside the function like you tried with this.

Instead, you would want

function iterable(arr) {
  const gen = Object.assign(function* () {
    while (gen.index < gen.arr.length) {
      yield gen.arr[gen.index++];
    }
  }(), {
    arr,
    index: 0,
    current() {
      return gen.arr[gen.index];
    },
  });
  return gen;
}

Alternatively, instead of using generator syntax you can also directly implement the Iterator interface:

function iterable(arr) {
  return {
    arr,
    index: 0,
    current() { return this.arr[this.index]; },
    next() {
      const done = !(this.index < this.arr.length);
      return { done, value: done ? undefined : this.arr[this.index++] };
    },
    [Symbol.iterator]() { return this; },
  };
}

(which you could of course write as a class as well)

Upvotes: 2

T.J. Crowder
T.J. Crowder

Reputation: 1074475

There seem to be multiple interpretations of this question. My understanding is that you want an iterator that provides a way to access the most recently-retrieved value, as shown by the last line in your final code block:

console.log(i.current()); // 0

Doing that isn't part of the iterator interface and isn't provided by generator functions. You could provide an iterator wrapper that did it, and then use that on the generator from the generator function (although you don't need a generator for what you're doing, the standard array iterator does it), see comments:

// Get the Iterator prototype, which has no global name
const itPrototype = Object.getPrototypeOf(
    Object.getPrototypeOf([][Symbol.iterator]())
);
function currentWrapper(source) {
    // Allow source to be an iterable or an iterator
    if (Symbol.iterator in source) {
        source = source[Symbol.iterator]();
    }
    // Create our wrapper iterator
    const it = Object.create(itPrototype);
    // Remember the last value we saw from `next`
    let current = null;
    // The iterator method
    it.next = () => {
        return current = source.next();
    };
    // Our additional methods
    it.current = () => current && current.value;
    it.currentResult = () => ({...current});
    return it;
}

This has the advantage of being reusable and generic, not tied to a specific iterable.

Live Example:

// Get the Iterator prototype, which has no global name
const itPrototype = Object.getPrototypeOf(
    Object.getPrototypeOf([][Symbol.iterator]())
);
function currentWrapper(source) {
  // Allow source to be an iterable or an iterator
  if (Symbol.iterator in source) {
    source = source[Symbol.iterator]();
  }
  // Create our wrapper iterator
  const it = Object.create(itPrototype);
  // Remember the last value we saw from `next`
  let current = null;
  // The iterator method
  it.next = () => {
    return current = source.next();
  };
  // Our additional methods
  it.current = () => current && current.value;
  it.currentResult = () => ({...current});
  return it;
}

// Something to iterate over
const a = [1, 2, 3];

// Example use #1: Using `current`
const it = currentWrapper(a[Symbol.iterator]());
console.log("current", it.current());             // undefined
console.log("next", it.next());                   // {value: 1, done: false}
console.log("current", it.current());             // 1
console.log("currentResult", it.currentResult()); // {value: 1, done: false}

// Example use #2: Just normal use of an iterator
for (const value of currentWrapper(a)) {
  console.log(value);
}
.as-console-wrapper {
  max-height: 100% !important;
}

I focussed on the current bit and not the index bit because I think of iterables as streams rather than arrays, but I suppose it would be easy enough to add index. The slightly-tricky part is when the iterator has finished, do you increment the index when next is called or not? The below doesn't:

// Get the Iterator prototype, which has no global name
const itPrototype = Object.getPrototypeOf(
    Object.getPrototypeOf([][Symbol.iterator]())
);
function currentWrapper(source) {
  // Allow source to be an iterable or an iterator
  if (Symbol.iterator in source) {
    source = source[Symbol.iterator]();
  }
  // Create our wrapper iterator
  const it = Object.create(itPrototype);
  // Remember the last value we saw from `next` and the current "index"
  let current = null;
  let index = -1;
  // The iterator method
  it.next = () => {
    // Don't increase the index if "done" (tricky bit)
    if (!current || !current.done) {
      ++index;
    }
    return current = source.next();
  };
  // Our additional methods
  it.current = () => current && current.value;
  it.currentResult = () => ({...current});
  it.currentIndex = () => index;
  return it;
}

// Something to iterate over
const a = [1, 2, 3];

// Example use #1: Using `current`
const it = currentWrapper(a[Symbol.iterator]());
console.log("current", it.current());             // undefined
console.log("next", it.next());                   // {value: 1, done: false}
console.log("current", it.current());             // 1
console.log("currentResult", it.currentResult()); // {value: 1, done: false}
console.log("currentIndex", it.currentIndex());   // 0
console.log("next", it.next());                   // {value: 2, done: false}
console.log("current", it.current());             // 2
console.log("currentResult", it.currentResult()); // {value: 2, done: false}
console.log("currentIndex", it.currentIndex());   // 1

// Example use #2: Just normal use of an iterator
for (const value of currentWrapper(a)) {
  console.log(value);
}
.as-console-wrapper {
  max-height: 100% !important;
}

Upvotes: 1

Nina Scholz
Nina Scholz

Reputation: 386654

Why not use a function from MDN Iterators and generators, where just the return part is replaced by the value instead of an object with value and done property

function makeIterator(array) {
    var nextIndex = 0,
        lastValue;

    return {
        next: function() {
            return lastValue = nextIndex < array.length ? array[nextIndex++] : undefined;
        },
        last: function () {
            return lastValue;
        }
    };
}

var it = makeIterator(['yo', 'ya']);
console.log(it.next());
console.log(it.next());
console.log(it.last());
console.log(it.next());

Upvotes: -1

Related Questions