Important notes
Debugging: How to find and fix mistakes
It is very easy to make mistakes when you have to handle variables, change them, keep track, etc. Especially in a loop. A great way to understand what's going on is to use the simplest debugging technique — console.log
. Recall, this function prints onto the screen whatever you pass to it.
For example, I'm trying to understand what's happening inside a while loop:
while (counter <= n) {
result = result * result;
counter = counter + 1;
}
I'm going to add console.log here:
while (counter <= n) {
result = result * result;
counter = counter + 1;
console.log(result)
}
Now, each time this block of code repeats, the result
variable will be printed. Let's see:
1
1
1
(I'm running the function with n
equals 3)
The result
in each step is 1. This is not right, the result
should increase each step... Okay, so, the problem is probably in the line where result
is changing. Right! I have result = result * result
, but I need to multiply result
by counter
, not by result
.
1
2
6
Now it works! I see the steps now, and the last step produces the correct answer: 3! is 6.
Don't hesitate to put console.log
wherever. It's your best friend :-)
Infinite loops
Since a while loop simply checks the condition, we can create infinite loops if we make the condition to be always true.
while (10 > 5) {
console.log("Ten is still larger than 5");
}
This will print "Ten is still larger than 5" until the heat death of the universe, or until you close the program, or until your computer runs out of memory, whatever comes first. Because the condition 10 > 5
is always true.
An even simpler way to make an infinite loop is while (true) { ... }
. true
is always true.
Sometimes your loops will be infinite, even if you didn't plan on that. This is a common problem, and it only means that inside the loop you have forgotten to change the thing that is being checked. For example, if I delete the line counter = counter + 1
from today's lesson's loop:
while (counter <= n) {
result = result * result;
}
... I'll end up with an infinite loop: counter never changes, so if counter <= n
, it is true forever.
Lesson notes
Variables are like constants, but you can change their values at any moment.
let age = 21;
age = 22; // now age is 22
age = age + 10; // now age is 32
Loops are repeated blocks of code. A while loop is a block repeated while some condition is true.
while (condition) {
do_stuff;
}
Here is the factorial function with a while loop:
const factorial = (n) => {
let counter = 1;
let result = 1;
while (counter <= n) {
result = result * counter;
counter = counter + 1;
}
return result;
}
Idea: make counter
= 1, then multiply result
by counter
repeatedly, while counting up to n
(the number passed to the function). When counter
becomes larger than n
— stop. By then, result
will be the answer.
This is iteration — defined repetition of code. Different languages have different ways to to iteration. A while loop is one of the ways JavaScript offers.
Declarative vs. Imperative
Compare a recursive factorial (from lesson 8) and a non-recursive factorial (from today):
const recursiveFactorial = (n) => {
if (n === 1) {
return 1;
}
return n * recursiveFactorial(n-1);
}
const factorial = (n) => {
let counter = 1;
let result = 1;
while (counter <= n) {
result = result * counter;
counter = counter + 1;
}
return result;
}
This recursive function is declarative — it's like a definition of factorial. It declares what factorial is.
This non-recursive iterative function is imperative — it's a description of what to do in order to find factorial. Declarative comes from Latin "clarare" — to make clear, to announce, to make a declaration. You declare — I want factorial of n to be n times factorial of n-1.
Imperative comes from latin "imperare", it means "to command". You command the steps precisely — multiply this and that while counting down and keep some numbers in mind.
Declarative is what. Imperative is how.
Writing declarative code is, in general, a better idea. Your code will be easier to read and understand, and it will be easier to build upon. But sometimes you have no choice.
Many bugs come from the changing state*, and assignment statements that perform changes are often the root cause of all the evil in the universe.
So, when it comes to the assignment statement, tread lightly.
Optional reading
- Iteration on Wikipedia
- Fundamentals of Programming: Iteration
Lesson transcript
We are already familiar with a way of naming things using constants. For example, here is a constant pi
with a value of 3.14
.
const pi = 3.14;
After this line, every time we see pi
we know it means 3.14
. It's called a constant because, well, it's constant, permanent. After this line pi
is always 3.14
, it won't ever change. This is why in the paper analogy I was using a pen after all.
This might seem restrictive, but this is actually rather good. Changing the things we create is difficult to deal with. Imagine writing code using some constant and not being sure it's what you think it is.
Nevertheless, sometimes you need to be able to change things you create or change their values, in other words. Say, you want to do something 5 times. One way is to do it repeatedly while counting to 5, and then stop. For this, you'd need something to store this counter, this changing number. A constant won't work — it's permanent, you can't change its value after creating.
This is why JavaScript and most other programming languages have a concept of a "variable". Think of a variable as a piece of paper with a name written with a pen, but a value written with a pencil. At any moment you can replace the value with a different one.
We can calculate a factorial with a variable like so:
let factorial = 1;
factorial = factorial * 2; // 2
factorial = factorial * 3; // 6
factorial = factorial * 4; // 24
factorial = factorial * 5; // 120
Creating a variable is easy, it looks like a constant, but instead of const
we say let
. We let it be a thing, and this is not forever.
Next, we change the value of factorial
. We couldn't have done this if factorial
was a constant. This line means "make the factorial be whatever factorial times 2 is". So JavaScript multiplies factorial by 2 and stores this result in the factorial
variable. Before factorial
was 1, and now it is 2.
We repeat this 3 more times, every time multiplying the number by the next integer: by 3, by 4 and by 5. This is what you might do in your head when multiplying numbers. You probably don't think about it explicitly, but this would be a way of explaining the process of computation if you were to explain it.
This idea of using a counter to repeat something multiple times is common in programming, and most programming languages have "loops" for this. Let's look at one particular type of a loop — a "while loop". It's a block of code that is repeated while some condition is met.
Think of a farmer who works from sunrise till sunset. In other words, he works while the sun is up. You could say:
while (sun is up) {
work
}
This is not valid JavaScript, of course, this is just to give you an idea. This "work" thing is going to be repeated again and again, while the sun is up. This means after each repetition we need to check if the sun is still up, and stop if it isn't. In other words: check, do if it's true, check, do if it's true, check... etc.
Okay, so here is the factorial function with variables and a loop instead of recursion.
const factorial = (n) => {
let counter = 1;
let result = 1;
while (counter <= n) {
result = result * counter;
counter = counter + 1;
}
return result;
}
Woah, what's going on here? First, we create two variables: one for the counter, to count from 1 to the upper limit, and the second — for the running result.
Then the main part begins — a while loop that repeats while the counter is less than or equal to n — the number passed to this function. The code that is repeated is simple: we change the values of our two variables. Running result is multiplied by the counter, and the counter itself is incremented by 1.
At some point this condition — "counter is less than or equal to n" — will become false, so the loop will no longer be repeating and the program will continue to the next thing — return result
. By then, the result will be the answer, because, during those repetitions in the loop, the result was being multiplied by 1, then by 2, then 3, etc, until n, whatever that is.
Let's see what the computer does step by step when we call factorial of 3.
- Take one argument — number 3, known as
n
inside - Create a variable
counter
, set its value to 1 - Create a variable
result
, set its value to 1 - Check: counter is 1, it's less than or equal to
n
, so - Multiply
result
bycounter
and put the answer — 1 — intoresult
- Add 1 to
counter
and put the answer — 2 — intocounter
- Go back and check:
counter
is 2, it's less than or equal ton
, so - Multiply
result
bycounter
and put the answer — 2 — intoresult
- Add 1 to
counter
and put the answer — 3 — intocounter
- Go back and check:
counter
is 3, it's less than or equal ton
, so - Multiply
result
bycounter
and put the answer — 6 — intoresult
- Add 1 to
counter
and put the answer — 4 — intocounter
- Go back and check:
counter
is 4, it's not less than or equal ton
, so don't repeat anymore and continue to the next line in the program - Return
result
— 6
Computer does this billion times faster but does the same thing. This kind of defined repetition is called an "iteration" in general. Our program uses iteration to calculate a factorial.
Last time we've seen an iterative process with recursion, and this time — an iteraritve process without recursion.
Both use the technique of iteration, but with recursive calls, we didn't have to change the values, we just passed the new values to the next function call. On the other hand, this factorial function doesn't have any recursive function calls, so all the transformations should happen inside a single instance, a single function box. We have no other choice but to change the values of things.
This is the first time in our course when we actually change the values of things. We haven't done this before but we were still able to compute.
The style of programming you've seen before this lesson is called "declarative". The style of programming you've seen today, with changing values, is called "imperative".
Compare a recursive factorial and a non-recursive factorial:
const recursiveFactorial = (n) => {
if (n === 1) {
return 1;
}
return n * recursiveFactorial(n-1);
}
const factorial = (n) => {
let counter = 1;
let result = 1;
while (counter <= n) {
result = result * counter;
counter = counter + 1;
}
return result;
}
This recursive function is declarative — it's like a definition of factorial. It declares what factorial is.
This non-recursive iterative function is imperative — it's a description of what to do in order to find factorial.
Declarative comes from Latin "clarare" — to make clear, to announce, to make a declaration. You declare — I want factorial of n to be n times factorial of n-1.
Imperative comes from latin "imperare", it means "to command". You command the steps precisely — multiply this and that while counting down and keep some numbers in mind.
Declarative is what. Imperative is how.
Writing declarative code is, in general, a better idea. Your code will be easier to read and understand, and it will be easier to build upon. Some languages tend to push you towards one side or the other, and some even leave you no choice, but ultimately you have to learn to see when imperative approach gives you more problems than solutions. Keeping track of changing variables is hard, and even a handful of variables make the system too complex to think about. Many bugs come from the changing state, and assignment statements that perform changes are often the root cause of all the evil in the universe.
Credits
Music: http://www.bensound.com
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.