Iterators and iterables
Iterators are interfaces (Iterator<T> ) that support a next() operation, which, in turn, returns the following generic interface:
Here the done property must return true if and only if no more values are available in the iteration, and the value property must contain the current element of the iteration. Being an interface, we are free to implement Iterator<T> in the way we prefer. The following is an implementation that lists all integers up to a given value:
The getNumberIterator function returns an iterator that iterates until the value passed in its x parameter. In order to test iterables, we need to add two more declaration libraries to the "lib" TypeScript compiler option, since we are targeting "es5", and not "es6". For the same reason, we need to set the downlevelIteration compiler option to true in order to make that TypeScript compiler translate a for ...of ES6 loop into ES5-equivalent code:
The getNumberIterator function snippet must be inserted inside an Iterables namespace into an Iterables.ts file under the .ts folder:
Then, in order to execute the TypeScript code, replace the previous Scripts section in Views/Home/index.cshtml with the following:
Now the iterator5 iterator can be used with a while loop:
That is, iterables are objects containing a [Symbol.iterator]() method that returns an iterator. It is worth pointing out that Symbol.iterator is a well-known symbol. The previous example may be redefined as follows:
Iterables may be processed by the for...of loop, which is the TypeScript equivalent of a C# foreach...in loop:
The whole technique for defining an iterable through the explicit creation of an iterator and its next() method appears quite cumbersome. The definition of the [Symbol.iterator] method is also quite evolved from the simple NumberIterable example that simply iterates over integers.
There are a lot of nested functions and objects; for instance, a function that returns an iterator whose next method returns another object. The next subsection analyzes a technique that simplifies the definition of iterables by avoiding the explicit definition of iterators.
Generator functions and iterables
The source of the whole complexity of the iterator-next-method technique for defining an iterable is the demand to decouple the code of the iterable from the code that uses all the values produced by the iterable. Without this demand, the entire code of the NumberIterable example would collapse into trivial code:
The complication of using a next method is necessary to carry out this code decoupling. The next method call is the way in which producer and consumer of values synchronize, the producer remains in a waiting state until the consumer requires a new value by calling the next method, then it resumes running in order to produce the new value, and then it goes back to a waiting state again.
Notwithstanding the complications associated with the next level iterator, simple examples such as NumberIterable are quite easy to code, but what if you require a more complex enumeration?—For example, an iterable that enumerates all descendants of a DOM node, in other words, the entire subtree of DOM elements that are under a given node.
Processing all the nodes of a tree can be executed by means of a simple recursive procedure, but what about implementing it with the iterator? It would be quite hard because you should transform recursion into iteration and then encode nested iterations with the iterator. This can be quite complex, likelihood of errors.
Luckily, generator functions solve the problem of synchronizing producers and consumers in a smart way. The producer code is written as if the producer and consumer were not decoupled, that is, with loops and/or recursive calls.
However, whenever a new item x is computed, they execute the special instruction yield x. This instruction makes the new value available to the consumer and then it automatically freezes the producer code execution. Execution is resumed when the consumer requires a new item, and continues until the next yieldinstruction is encountered. At that point, the new value is returned to the consumer, and producer execution is frozen again.
With this technique, the core code of the generator function for the NumberIterable example becomes something very simple:
This is almost identical to the code needed when there is no producer-consumer decoupling!
A function is declared to be a generator function by appending an asterisk to the function keyword, as can be seen in the following:
In case the function keyword is not required, as in the example of class methods, it is sufficient to precede the method definition with an asterisk:
The Iterator<T> wrapper takes care of resuming and suspending code execution as required, when the consumer code calls the next method of the Iterator<T> wrapper. By way of clarification, let's create a generator function-based implementation of the NumberIterable iterable:
After this definition, the consumer code may process all the values returned by the iterable in the exact same way as with the previous implementation:
Generator functions can also use a yield* instruction that, instead of returning a single value, moves the computation path to the values produced by another iterable. The yield* instruction allows the transformation of recursion-based visits into generator functions. Let's see how yield* works with the previously mentioned DOM tree iterable that enumerates the DOM subtree rooted in a given node:
DOMTreeIterable returns the root node, and then recalls itself on each child element with yield*. The collection of child elements is iterated with a for ...of.. loop, since all DOM collections are iterables themselves. However, in order to enable TypeScript to accept for ...of.. loops for DOM collections, the "dom.iterable" library must be added to the "lib" compiler option as follows:
Inheriting from Array<T>
For instance, it is possible to define a subclass of Array<T> that has one more constructor accepting an iterable as its unique argument:
The EnhancedArray<T> subclass provides the same two standard constructors of Array<T> and then adds the new one. The elements of the iterable passed as arguments are pushed to the subclass one after the other.
Subclasses of Array<T> may define the [Symbol.isConcatSpreadable] computed Boolean property. If it returns true, the elements of any instance of the subclass are flattened with all elements of other arrays when the subclass is involved in an array concat operation.
If [Symbol.isConcatSpreadable] returns false, the entire subclass is considered a single non-array element and its elements are not flattened during array concatoperations. Thus, for instance, if you add the following code to the EnhancedArray<T> class, then instances of EnhancedArray<T> will never have their elements flattened in array concat operations:
The next subsection describes new collections introduced by ECMAScript 2015.
ECMAScript 2015 built-in iterables
ECMASCript 2015 has built-in iterables that implement the abstract dictionaries and set data structures. In order to use them in TypeScript, the "es2015.collection" declarations library must be added to the "lib" compiler option:
Built-in iterables are supported by all mainstream browsers with the exception of Internet Explorer (just Internet Explorer, not Edge), which offers partial support for Map, Set, and WeakMap, and doesn't support WeakSet at all.
Partial support refers to the absence of all iterable-related features, since, as has already been pointed out, Internet Explorer doesn't support iterables. All built-in iterables have a constructor that either accepts no argument, an array, or an iterable (Internet Explorer only accepts an array).
In the case of Set and WeakSet, the iterable contains all the elements to insert initially into the Set (WeakSet), while, in the case of Map and WeakMap, the iterable is composed of two-element tuples whose first element is the dictionary key and whose second element is the corresponding value.
A review of each ECMAScript 2015 built-in iterable follows. As regards the examples used in this subsection, please create a new TypeScript file named BuiltInIterables.ts under the .ts folder.
Map<K, V> and WeakMap<K, V>
Map<K, V> is a key-value pair dictionary. For example, you may use it to create a book of friends that associates a Person object with each friend's name that represents them and contains all their data:
New key-value pairs are added using the set method:
Values are then retrieved through their associated keys using the get method:
The size property returns the number of key-value pairs stored in Map:
The forEach method invokes a callback on each pair contained in Map:
The callback is passed, along with the value, the key, and the whole Map.
The delete method deletes a pair from Map through its key:
The clear method deletes all pairs (though it is, usually, more efficient to create a new empty Map). The entries property contains an iterable with all key-value pairs represented as two-element tuples. The keysproperty contains an iterable that enumerates all keys, while the values property contains an iterable that enumerates all values.
WeakMaps are useful for encoding temporary associations between objects used in a computation, so that when that computation ends and some of those objects are no longer used, they must also be removed from all the maps they are in so that the garbage collector may free up their memory.
Set<T> and WeakSet<T>
Set<T> is an implementation of a mathematical finite set. Accordingly, it is a collection with no repetitions, so if the same value is added several times, only the first occurrence is taken.
Values are added using the add method:
The has Boolean method tests whether a value is contained in Set:
The size property returns the number of values contained in Set:
The forEach method invokes a callback on each value contained in Set:
A value may be removed from a set using the delete method:
The clear method deletes all values from a set (though it is, usually, more efficient to create a new empty set). Finally the values property contains an iterable that enumerates all values.