While I looked for an interesting programming article, I found a programming exercise. Interestingly, Symbol.iterator
is used in the answer. I’ve never used it in my work, so I tried to use it to learn how it works.
The exercise I tried is something like this.
Select an element randomly from an array ["One", "Two", "Three"]
. If the order matches ["One", "Two", "Three"]
repeated 3 times, attempt count and the actual result should be shown on the console.
How do you implement it?
Implementation without Symbol.iterator
Firstly, let’s implement it without Symbol.iterator
.
const ONE_TWO_THREE = [
"One",
"Two",
"Three",
] as const;
type StringNumType = typeof ONE_TWO_THREE[number];
const ANSWER: StringNumType[] = [
...ONE_TWO_THREE,
...ONE_TWO_THREE,
...ONE_TWO_THREE,
];
function getNext(): StringNumType {
return ONE_TWO_THREE[Math.floor(Math.random() * ONE_TWO_THREE.length)];
}
function func(): void {
let attemptCount = 0;
let answerIndex = 0;
const actualStringArray: StringNumType[] = [];
console.log("Start")
while (true) {
attemptCount++;
const currentString = getNext();
if (ANSWER[answerIndex] !== currentString) {
actualStringArray.splice(0, actualStringArray.length);
answerIndex = 0;
continue;
}
actualStringArray.push(currentString);
answerIndex++;
if (answerIndex === ANSWER.length) {
break;
}
}
console.log(`attemp count: ${attemptCount}`);
console.log(`Answer: ${actualStringArray.join(",")}`);
}
// An example result
// Start
// attemp count: 52189
// Answer: One,Two,Three,One,Two,Three,One,Two,Three
If you understand everything, you can skip to the next chapter. I will explain the code above from here.
const ONE_TWO_THREE = [
"One",
"Two",
"Three",
] as const;
We need to prepare 3 strings. Even if const keyword is used, the array elements can be updated by using the method like push()
and splice()
. By adding as const
, the variable becomes read-only and it’s impossible to change the element.
Without as const
, the variable becomes an array and can be broken. This is important if you don’t want to have accidental change.
// "One" | "Two" | "Three"
type StringNumType = typeof ONE_TWO_THREE[number];
It creates a new type from an object (array). typeof
keyword can create a new type from an object. By passing number
to the array, it can extract the elements. I explained typeof
in the following article.
const ANSWER: StringNumType[] = [
...ONE_TWO_THREE,
...ONE_TWO_THREE,
...ONE_TWO_THREE,
];
This array defines the desired output. It is ["One", "Two", "Three", "One", "Two", "Three", "One", "Two", "Three"]
. Three dots are called Spread Operator. If you need a more detailed explanation, go to this article.
function getNext(): StringNumType {
return ONE_TWO_THREE[Math.floor(Math.random() * ONE_TWO_THREE.length)];
}
This function returns the next value. Math.floor
returns the largest integer less than or equal to a given number.
- 0.3 -> 0
- 1.44 -> 1
- 0. 99 -> 0
- 1.99 -> 1
Math.random
returns 0 – 1 but we want 0 or 1. We need to return a value between 0 – 2, and thus, ONE_TWO_THREE.length
is multiplied.
function func(): void {
let attemptCount = 0;
let answerIndex = 0;
const actualStringArray: StringNumType[] = [];
...
}
It defines the main function and initializes the necessary variables.
while (true) {
attemptCount++;
const currentString = getNext();
...
}
When it gets the next value, the count must be incremented. The next value is assigned to currentString
.
while (true) {
attemptCount++;
const currentString = getNext();
if (ANSWER[answerIndex] !== currentString) {
actualStringArray.splice(0, actualStringArray.length);
answerIndex = 0;
continue;
}
...
}
It gets an expected value from ANSWER
array and compare the two values. If the currentString
doesn’t have an expected value, the two variables actualStringArray
and answerIndex
are initialized.
Since actualStringArray
is const variable, it’s impossible to re-assign an empty array []
. Thus, splice
method is used here to clear the current elements.
By continue
keyword, the program goes back to the top of the while loop.
while (true) {
...
actualStringArray.push(currentString);
answerIndex++;
if (answerIndex === ANSWER.length) {
break;
}
}
If the currentString
has an expected value, store the result and increment answerIndex
so that it can extract the next expected value. If the answerIndex
reaches the end of the array, it goes out from the while loop.
function func(): void {
...
console.log(`attemp count: ${attemptCount}`);
console.log(`Answer: ${actualStringArray.join(",")}`);
}
Then, output the result.
Using Symbol.iterator for the initialization
Let’s see the second implementation.
// Same code from here -----
const ONE_TWO_THREE = [
"One",
"Two",
"Three",
] as const;
type StringNumType = typeof ONE_TWO_THREE[number];
const ANSWER: StringNumType[] = [
...ONE_TWO_THREE,
...ONE_TWO_THREE,
...ONE_TWO_THREE,
];
function getNext(): StringNumType {
return ONE_TWO_THREE[Math.floor(Math.random() * ONE_TWO_THREE.length)];
}
// Same code up here -----
function initializeIterator(): Iterator<StringNumType> {
return ANSWER[Symbol.iterator]();
}
function func2(): void {
let attemptCount = 0;
const actualStringArray: StringNumType[] = [];
let iterator = initializeIterator();
console.log("Start")
while (true) {
attemptCount++;
const nextAnswer = iterator.next();
if (nextAnswer.done) {
break;
}
const currentString = getNext();
if (nextAnswer.value !== currentString) {
iterator = initializeIterator();
actualStringArray.splice(0, actualStringArray.length);
continue;
}
actualStringArray.push(currentString);
}
console.log(`attemp count: ${attemptCount}`);
console.log(`Answer: ${actualStringArray.join(",")}`);
}
I’ve never seen Symbol
used in a real project. I had no idea how to use Symbol but this is one of the good examples.
function initializeIterator(): Iterator<StringNumType> {
return ANSWER[Symbol.iterator]();
}
This function returns an object that implements iterator. A caller side can get the next value by calling iterator.next()
method.
Symbol is a unique identifier. If we use a string as a key, there can be a collision if someone uses the same string. By using Symbol, we can avoid the case.
When a cursor is on Symbol.iterator
, the following explanation is shown.
(property) SymbolConstructor.iterator: typeof Symbol.iterator
A method that returns the default iterator for an object. Called by the semantics of the for-of statement.
It is used in for-of
.
function func2(): void {
let attemptCount = 0;
const actualStringArray: StringNumType[] = [];
let iterator = initializeIterator();
...
}
The first two lines are the same as before. The last statement gets an iterator object.
while (true) {
attemptCount++;
const nextAnswer = iterator.next();
if (nextAnswer.done) {
break;
}
...
iterator.next()
returns the next iterator result object. The object has done and value properties. If it doesn’t have the next value, done
variable becomes true. It means the loop finishes.
const currentString = getNext();
if (nextAnswer.value !== currentString) {
iterator = initializeIterator();
actualStringArray.splice(0, actualStringArray.length);
continue;
}
What it does is basically the same as the previous code. Compare the current value and the expected value. If it’s different, it initializes the variables.
iterator
is also initialized so that the next iterator.next()
call returns the first element of ANSWER
variable.
How is Symbol.iterator is defined in the interface
Let’s see the initializeIterator
function again.
function initializeIterator(): Iterator<StringNumType> {
return ANSWER[Symbol.iterator]();
}
It looks a function call. Why do we need “()
” at the end? If we check the interface, it is defined in the following way.
interface ReadonlyArray<T> {
/** Iterator of values in the array. */
[Symbol.iterator](): IterableIterator<T>;
It is NOT ANSWER[Symbol.iterator]
. It must be ANSWER[Symbol.iterator]()
. It returns an iterable iterator and we can call it as many as we want when we want to go back to the first element.
By the way, Iterator is different from Iterable. They are defined in the following way.
interface Iterator<T, TReturn = any, TNext = undefined> {
// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
}
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator](): IterableIterator<T>;
}
Iterable is an interface that returns an iterator.
ANSWER[Symbol.iterator]()
returns an IterableIterator
object. If Symbol.iterator
is used again, it means that the same object is returned. See the following example.
const array = ["11", "22", "33", "44"];
let iterator = array[Symbol.iterator]();
console.log(iterator.next()); // { value: '11', done: false }
console.log(iterator.next()); // { value: '22', done: false }
let iterator2 = iterator[Symbol.iterator]();
console.log(iterator2.next()); // { value: '33', done: false }
console.log(iterator2.next()); // { value: '44', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
It gets an iterator and call next()
method twice. And then, it gets an IterableIterator
object by calling iterator[Symbol.iterator]()
. next()
method call for the second object returns the next object of the first iterator. Both variables have the same reference.
But if we get an object from array
variable again, it returns to the top element again.
iterator = array[Symbol.iterator]();
console.log(iterator.next()); // { value: '11', done: false }
Conclusion
By using Symbol.iterator
, we can extract an iterator object from an array.
iterator = array[Symbol.iterator]();
let iteratorResult = iterator.next();
while (!iteratorResult.done) {
// do something with iteratorResult.value
iteratorResult = iterator.next();
}
I didn’t have a good idea of how to use Symbol but this is a good example.
Which code do you prefer? I guess not many developers know Symbol.iterator
and thus the second implementation is less readable. I didn’t understand how it worked at the first glance.
This technique is good to know but if we want to use it in a real project where there are several developers, we should have a dev-exchange meeting for it.
In this exercise, it doesn’t improve the maintainability very much. I prefer the first implementation.
Comments