Friday 24 November 2017

Multiple Iteration

I've come across with a pretty interesting difference in the behaviour of JavaScript generator functions/methods and that of C# iterator methods. Though the names are different (a very wrong decision on Microsoft side, using enumerable/enumerator/iterator rather than iterable/iterator/generator, that is what most other languages do), in both cases we talk about those magic funtions/methods that contain some yield statement and that the compiler will translate into a state machine.

In Javascript a generator function returns a generator object, an object that is both an iterable and an iterator. When you ask the generator object for an iterator (via [Symbol.iterator]) it returns itself. Because of this, trying to iterate multiple times on the same generator object will result in that the iteration will only take place the first time, for the ensuing ones, as the iterator is already at the end, no iteration will happen. I mean:

function* getCities(){
 yield "Toulouse";
 yield "Xixon";
 yield "Berlin";
}

let citiesGenOb = getCities();

let citiesIterator1 = citiesGenOb[Symbol.iterator]();

console.log(citiesGenOb === citiesIterator1 ? "same object" : "different object"); //same object

console.log("- first iteration:");
for (let city of citiesGenOb){
 console.log(city);
}

console.log("-------------");

console.log("- second iteration:");
//no iteration is done, citiesGenOb[Symbol.iterator] is returning the same object
//that was already iterated to the end in the previous loop
//very interesting, this behaviour is different from C#, here the generator object (this is both iterable and iterator) is returning itself, rather than a copy
for (let city of citiesGenOb){
 console.log(city);
}

let citiesIterator2 = citiesGenOb[Symbol.iterator]();
console.log(citiesGenOb === citiesIterator2 ? "same object" : "different object"); //same object


/*
same object
- first iteration:
Toulouse
Xixon
Berlin
-------------
- second iteration:
same object
*/

The generator and the iterator (returned by the implicit call to citiesGenOb[Symbol.iterator] done by the "for...of" loop) are the same object, so once we have iterated the first time, the iterator is at the end and trying to iterate it again will iterate nothing.

In C#, an iterator method returns an object of a class created by the compiler and that implements both the IEnumerable and IEnumerator interface. Based on the Javascript behaviour one could expect that invoking the GetEnumerator method of an instance of this class would return the same instance, but the C# compiler is doing different, let's see some code:

private static IEnumerable<string> GetCountries()
{
 yield return "France";
 yield return "Belgium";
 yield return "Portugal";
}

IEnumerable<string> countries = GetCountries();
var enumerator1 = countries.GetEnumerator();

Console.WriteLine((enumerator1 == countries) ? "Same reference" : "Different reference"); //Different

//the Iterator method is returning an IEnumerable/IEnumerator object, but the thing is that calling to GetEnumerator returns a new instance, rather than the object itself
//Because of that the 2 loops do a whole iteration, and enumerator1 and 2 are different objects.
Console.WriteLine("- first iteration:");
foreach(string country in countries)
 Console.WriteLine(country);

Console.WriteLine("- second iteration:");
foreach(string country in countries)
 Console.WriteLine(country);

enumerator1 = countries.GetEnumerator();

Console.WriteLine((enumerator1 == countries) ? "Same reference" : "Different reference"); //Different


var enumerator2 = countries.GetEnumerator();
Console.WriteLine((enumerator1 == enumerator2) ? "Same reference" : "Different reference");

/*
Same reference
- first iteration:
France
Belgium
Portugal
- second iteration:
France
Belgium
Portugal
Different reference
Different reference
*/  

What seems to happen is that when you call GetEnumerator in your IEnumerable/IEnumerator object, if the object has not been iterated yet, you obtain the same object (that's the first case, where I get "Same reference"), but if it has alredy been enumerated, you get a new instance!. This way you can iterate multiple times the same IEnumerable/IEnumerator object (as you can see, both "first iteration" and "second iteration" do a complete iteration).

Decompiling the IEnumerable/IEnumerator class created by the compiler (instances of which are created by the Iterator method) we can see:

Notice that in Javascript if we have function that expects an iterable that it will iterate several times, rather than passing it a generator object, we should pass the generator function itself, so we can invoke it each time we want to perform the iteration.

No comments:

Post a Comment