FizzBuzz
The classic. For the numbers 1 to 15, push "Fizz" into out if the number divides by 3, "Buzz" if it divides by 5, "FizzBuzz" if both, and otherwise the number itself as text.
LEARN TO PROGRAM · CHAPTER 03 · BASICS
In chapter 1 the program ran from top to bottom, one line after another. Real programs don't: they decide (do this or that) and they repeat (do this ten thousand times). The order in which the lines actually run is the program's control flow.
Picture the code as a railway and the running program as a token riding it. Arches are statements. Switches are decisions, with a lamp that goes green for true and red for false. Loops are tracks that bend back on themselves. Scroll, and the token rides each layout, running a real program with real values.
01 · SEQUENCE
The simplest flow is a straight line. Each statement runs once, in order, and each one sees the world the one before it left behind. On the stage, the token passes under four arches, and the chip above it shows x changing: 5, then 6, then 12.
Press Step through: the highlighted line is the arch the token is under.
Order matters. The same two steps, swapped, give a different answer, because x is a different number by the time each one runs:
02 · IF / ELSE
An if statement asks a yes-or-no question and picks a track. If the condition is true the token takes the first branch; if not, it goes on to the else. Chain else ifs for more choices: they are tried top to bottom, and the first true one wins. Everything after it is skipped.
On the stage the program runs three times, with t at 30, 20 and 5. Watch the first switch go green for 30, red then green for 20, and red twice for 5. All three tracks meet again before console.log: an if splits the flow, and the flow rejoins after it.
Change 30 to 20, then 5, then exactly 25. Is 25 "hot"? Why not?
The condition can be anything that gives true or false. Comparisons are the usual source:
Use === and !==. The two-equals version converts types first, which surprises everyone eventually.
03 · && AND ||
Questions combine. AND (&&) is true only when both sides are true. OR (||) is true when at least one side is. NOT (!) flips true and false. A truth table lists every combination; there are only four for two inputs, and the board at the back of the stage lights the row being tested.
Now watch the two layouts closely. The AND gate is two switches in a row: the token reaches B only if A was true. The OR gate is two switches side by side: if A is true, there's already a way through, and B is never asked. When a switch is skipped, its sign dims and says so.
That is short-circuit evaluation: JavaScript stops as soon as it knows the answer, and doesn't even run the right-hand side. This is in the language specification, not an optimisation you hope for, so you can rely on it:
Count the "checking" lines. B was never checked either time.
The everyday use is a guard: check that something exists before you look inside it.
JavaScript is relaxed about what counts as a condition. Any value can go in an if; it is converted to true or false. The values that count as false are called falsy, and there are only a handful. Everything else, including "0" and empty arrays, is truthy:
false, 0, -0, 0n, "", null, undefined, NaN, and the oddity document.all. Every other value is truthy. MDN: Falsy04 · SWITCH
When you compare one value against a list of options, a switch reads better than a long else if chain. It works out the value once, then jumps to the case that matches it with ===, or to default if none does. On the stage a turntable swings to the right lane.
The catch: a case doesn't end on its own. Execution carries on into the next case's code until it hits a break. This is called fall-through. Watch "amber": its lane has no code and no break, so the track runs straight into the red lane and the token does what red does. Used on purpose, that's a neat way to share code.
Used by accident, it's a classic bug. Predict the output before you run it:
Newer languages learned from this. Go and Swift stop at the end of each case unless you write fallthrough; Python added match in version 3.10 with no fall-through at all:
t = 30
if t > 25:
msg = "hot"
elif t > 15:
msg = "mild"
else:
msg = "cold"
for i in range(3): # 0, 1, 2: the end is excluded
print(i)
match msg: # Python 3.10+: no fall-through
case "hot" | "mild":
print("go outside")
case _:
print("stay in")
05 · LOOPS
A loop is a decision at the top of a track that bends back to it. Each time round is called an iteration (or a lap). The for loop puts the three parts on one line: the start (let i = 0), the test asked before every lap (i < 3), and the update after every lap (i++). On the stage, the token passes the start once, then circles: test, body, update, back along the violet track. When the test turns red it leaves on the exit track.
Press Step through and watch the order: test, body, update, test, body… The test runs 4 times for 3 laps.
A while loop is the same machine with only the test on top. Use it when you don't know in advance how many laps it will take:
Most of the time you just want each item of a list. for…of hands you the items one at a time, so there's no counter to get wrong. The crates at the back of the stage light up one per lap:
do…while puts the test at the bottom, so the body always runs at least once:
06 · BREAK · CONTINUE
Two words change a loop's track from inside. break leaves the loop at once, onto the exit (the pink track). continue abandons the rest of this lap and goes straight back to the top for the next one (the short violet track).
On the stage, the loop walks through [3, 8, -1, 7]. 3 is odd, so it's added. 8 is even: continue sends the token back before the addition. -1 is negative: break leaves the loop, so 7 is never even looked at. The sum is 3.
Step through it and watch which lines run for each x. Then move -1 to the end of the list.
break only leaves the innermost loop. To leave an outer one from inside an inner one, give the outer loop a label:
07 · NESTED LOOPS
A loop body can contain anything, including another loop. The inner loop then runs all of its laps on every lap of the outer one. On the stage the inner ring spins three times for each trip round the outer ring, and the grid fills in row by row: a times table.
Make it 9 × 9 by changing both 3s.
That multiplication is the thing to remember. An outer loop of n laps around an inner loop of n laps does n × n steps. Ten items is a hundred steps; a thousand items is a million:
This is the first taste of Big-O: work that grows like n² is fine for small lists and painful for big ones. Chapter 6 is all about it.
08 · OFF BY ONE
Arrays count from 0. An array of length 5 has slots 0, 1, 2, 3 and 4, and no slot 5. Write the test as i <= arr.length and the loop takes one lap too many. On the stage the token runs the buggy loop first: the sixth lap reaches for the slot that isn't there and gets undefined. Then it runs again with <, and stops after five.
Fix the test and run it again. Then try starting at i = 1: which element goes missing?
Its old name is the fencepost error: a 100 m fence with a post every 10 m has 10 sections but 11 posts. Counting the gaps when you meant the posts, or the other way round, is the same mistake.
09 · LOOP VS RECURSION
There's a second way to repeat: a function that calls itself with a smaller problem, until the problem is so small the answer is obvious (the base case). That is recursion. On the stage the left token loops round a ring; the right token goes down a staircase. Each step down is a new call with its own copy of n, stacked on top of the one that called it. At countdown(0) it hits the base case, then climbs back up through every waiting call.
Press Step through and watch the call stack: countdown appears four times, one on top of another.
Both print 3, 2, 1. The loop needs one variable, however many laps it runs. The recursion needs a stack frame per waiting call, and the stack has a limit:
Recursion shines when the data itself is shaped like a tree (folders inside folders, the parse tree from chapter 1), where a loop would need you to manage a stack by hand. Some languages turn a call in "tail position" into a jump, so it costs no stack. The JavaScript standard has required that since 2015, but Safari is the only major browser that does it. Chapter 4 takes the call stack apart properly.
10 · FLOW GRAPH
Strip the railway down to its junctions and you get the tool compilers actually use: the control-flow graph. Cut the code into basic blocks, straight runs of statements with one way in (at the top) and one way out (at the bottom). Then draw an arrow for every way control can pass from one block to another. A condition has two arrows out, labelled T and F. A loop shows up as a back edge: an arrow to a block that came earlier.
let sum = 0;
for (let i = 1; i <= 4; i++) {
if (i % 2 === 0) {
sum += i;
}
}
console.log(sum);
On the stage this program becomes six blocks. B0 sets up; B1 is the loop test; B2 is the if; B3 adds; B4 is i++, with the back edge to B1; B5 prints. The token takes the F arrow from B2 on odd laps and the T arrow on even ones, and leaves along B1's F arrow when i reaches 5. It prints 6.
Step through it and name the block each highlighted line belongs to.
Every layout in this chapter was a control-flow graph in disguise. Compilers use them to find code that can never run, to spot variables used before they're set, and to find loops to speed up.
11 · JUMPS IN THE CPU
A processor knows nothing about if, while or switch. It has a program counter (PC) holding the address of the next instruction, and by default the PC just moves on to the next one: sequence. Control flow is done with jumps, instructions that load a new address into the PC. A conditional jump does that only if a condition holds; otherwise the PC carries on as usual.
Here is a C loop and roughly what a compiler turns it into on x86-64. The compiler flipped the test: "keep going while i < 3" became "jump out if i >= 3". cmp subtracts and sets flags; jge reads them. On the stage the PC token rides down the instructions, leaps back along jmp loop three times, and finally takes jge done.
#include <stdio.h>
int main(void) {
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += i;
}
printf("%d\n", sum);
return 0;
}
mov ecx, 0 ; i = 0
mov eax, 0 ; sum = 0
loop: cmp ecx, 3 ; compare i with 3 (sets flags)
jge done ; if i >= 3, jump out
add eax, ecx ; sum += i
inc ecx ; i++
jmp loop ; back to the test
done: ret ; the answer is in eax
You can build this in a few lines. Here is chapter 1's stack machine with a JMP and a JZ (jump if zero) added. A loop is nothing more than pc = an earlier number:
Press Step through and watch pc: it climbs, then drops back to 1 each lap.
A modern core works on many instructions at once, in a pipeline, so it has to fetch the instructions after a branch before it knows which way the branch goes. It doesn't wait: it guesses, and carries on along that path. Guess right and nothing is lost. Guess wrong and all the work on the wrong path is thrown away and the pipeline refills: on recent Intel and AMD cores, that costs about 15 to 20 clock cycles.
The simplest good predictor is the 2-bit saturating counter on the right of the board. Each taken branch moves it one step up, each not-taken one step down, and the top two states predict "taken". Watch it with jge: three times not taken (correct guesses), then taken, which it gets wrong: a flush. Real CPUs combine many such counters with the history of recent branches, and get the great majority right.
The same branch, the same numbers, a different order. Random order gets about half the guesses wrong; sorted order gets 2 wrong.
if (data[c] >= 128) took 11.54 s on random data and 1.93 s once the data was sorted, purely because the branch became predictable. It's one of the most up-voted questions on Stack Overflow. Stack Overflow question 11227809Compilers and careful programmers sometimes remove the branch altogether, turning the decision into arithmetic. No jump, nothing to mispredict:
To watch a CPU fetch, decode and execute instructions one clock at a time, open Inside the CPU.
Change commands and predict where the robot ends before you run it.
PRACTICE
Type the code yourself rather than pasting it: producing an answer builds memory that reading does not. Use the hints one at a time.
The classic. For the numbers 1 to 15, push "Fizz" into out if the number divides by 3, "Buzz" if it divides by 5, "FizzBuzz" if both, and otherwise the number itself as text.
A year is a leap year if it divides by 4, except years that divide by 100, unless they also divide by 400. So 2024 is, 2023 isn't, 1900 isn't, 2000 is. Make isLeap return the right true or false using &&, || and !, without any if.
This should add up the five prices and print 150. It prints NaN instead. Find the bug and fix it with the smallest change you can.
Set found to the index of the first negative number, and count in looked how many numbers the loop looked at. Use break so it stops at the first one: the answer is found = 3 after looking at 4 numbers.
Make kind(day) return "weekend" for "sat" and "sun", "weekday" for "mon" to "fri", and "not a day" for anything else. Use one switch, and let cases share code by falling through.
Use a loop inside a loop to build four rows: *, **, ***, ****. Push each row into rows. The inner loop has to know which row it is on.
sumTo adds 1 + 2 + … + n by calling itself. It works for 100 but runs out of stack for 100,000. Write sumLoop(n) that gives the same answers with a loop, so it works for both.
RECALL
Answer each one before you open it. Then come back tomorrow, and again next week, and answer them cold: retrieval spaced over days is the most reliable study method we know of. Why that works.
What are the three building blocks every structured program is made of?
In a && b, when is b never evaluated? And in a || b?
What happens in a switch when a case has no break?
What's the difference between break and continue?
An array has length 5. Which indexes are valid, and what loop test should you write?
What does an if statement become in machine code?
Why can sorting data make a loop with an if in it faster?
GO FURTHER
ON THIS SITE
SOURCES