Your first function
Write toFahrenheit: it takes degrees Celsius and returns degrees Fahrenheit (multiply by 9, divide by 5, add 32). It should return, not print.
LEARN TO PROGRAM · CHAPTER 04 · BASICS
A function is a few lines of code with a name. Write them once, then run them whenever you like by saying the name, with different values each time. Nearly every program is built from functions calling functions.
This chapter starts there and goes all the way down: how values go in and come out, which names a function can see, how the computer remembers where it was (the call stack), what happens when a function calls itself, and how to pass functions around like any other value. Scroll, and the 3D follows.
01 · NAMED STEPS
You already know one: a recipe. "Make the sauce" is a name for a list of steps, and the main recipe just says the name instead of repeating them. A function does the same for code. You define it once, with the keyword function, a name, and a body in braces. Then you call it by writing its name followed by brackets.
On the stage, area is a machine. Each call drops two values into its hoppers, the body runs (the gears), and one value slides out of the chute. The caller on the left doesn't need to know how it works inside, only what goes in and what comes out.
Both halves print the same thing. Now imagine the rule changes (say, areas must include a 1 cm border). With the function, you fix one line.
Why bother? Three reasons, which matter more as programs grow. No repetition: one fix fixes every use. A name explains intent: area(4, 3) says what it means, 4 * 3 doesn't. Hiding detail: you can use Math.sqrt without knowing how square roots are computed. That last idea is called abstraction, and it is how people build programs too big for one head.
Every language has functions; they look much the same everywhere:
def area(width, height):
return width * height
print(area(4, 3)) # 12
int area(int width, int height) {
return width * height;
}
printf("%d\n", area(4, 3)); /* 12 */
02 · IN AND OUT
The names in the definition's brackets are parameters: blanks to be filled. The values in a call are arguments: what fills them. In area(4, 3), the argument 4 lands in the parameter width and 3 in height, by position. On the stage the hoppers are labelled with the parameter names, and show the value each call drops in.
return does two things: it hands a value back to the caller, and it ends the function right there. A function with no return still finishes, but hands back undefined.
Change sign(-3) to sign(-0). Then try calling greet() with no argument at all, and read what comes back.
One subtle thing, worth learning now: parameters get copies of values. A number is copied, so the function can't change the caller's variable. But the "value" of an array or object is a reference to it, so the function receives a second arrow pointing at the same thing, and can change it.
Press Step through and watch shopping change while you are inside addItem.
03 · SCOPE
A variable made inside a function is local: it exists only inside that function, and only while it runs. That is what lets you call two different functions' variables i without them clashing. The region of code where a name exists is its scope.
Scopes nest, like glass rooms inside rooms. Code inside can see out through every wall; code outside can't see in. When inner uses a name, JavaScript looks in inner's room first, then outer's, then the global scope, and stops at the first match. On the stage the green probe goes looking for street, city and planet in turn, then a probe from outside tries to look in and is stopped at the wall.
Delete the const city line and run it: the error tells you the lookup reached the global scope and found nothing.
This is called lexical scope: which names you can see depends on where the code is written, not on who calls it. Since 2015, let and const also make a scope out of every { } block, which is why modern code avoids the older var.
A name declared inside can shadow (hide) the same name outside. Legal, but easy to confuse yourself with.
04 · CLOSURES
Functions are values: you can store them in variables and return them from other functions. Put that together with scope and something surprising happens. makeCounter makes a local count, then returns a little function that uses it. makeCounter has finished, so count should be gone. It isn't.
Press Step through: each call of a() finds count where it left it.
The returned function carries a link to the scope it was born in. That pair, a function plus the variables it can see, is a closure. On the stage, each function is tethered to its own count; the frame that created it fades away, but the variable stays, because something can still reach it. a and b came from separate calls, so they have separate counts.
Closures are everywhere in real code: every click handler that remembers which button it belongs to, every callback, and the simplest way to keep data private:
05 · THE CALL STACK
When sumOfSquares calls square, it pauses half-way through a line. When square returns, the computer must know where to carry on, and what a, b and x were. It keeps that in a stack frame: one box per running call, holding its parameters, its local variables and its place in the code.
Frames pile up like plates: a call puts a new one on top, a return takes the top one off and hands its value to the one below. Last in, first out: a stack. Watch the tower: main, then sumOfSquares(3, 4), then square(3) on top; it returns 9 and pops; square(4) takes its place and returns 16; then 25 drops to the bottom.
Press Step through and watch the "inside" line: it is the call stack, bottom to top, exactly as the tower shows it.
The stack is also how an error knows where it happened. A JavaScript Error takes a snapshot of the stack when it is made: the "stack trace" you will read every day as a programmer.
Top of the stack first: c was running, called from b, called from a.
Underneath, it is real memory. On an x86-64 CPU, a call instruction pushes the return address onto a stack in memory and jumps; ret pops it and jumps back. Here is what a C compiler (gcc, no optimisation) makes of square:
square:
push rbp ; save the caller's frame pointer
mov rbp, rsp ; this frame starts here
mov [rbp-4], edi ; store n in the frame
mov eax, [rbp-4]
imul eax, eax ; n * n, the return value goes in eax
pop rbp ; drop this frame
ret ; pop the return address and jump back
sumOfSquares:
...
call square ; push the return address, jump to square
mov ebx, eax ; x = what square returned
The 6502 in Inside the CPU does the same with JSR and RTS, and keeps its whole stack in one 256-byte page of memory. The ECMAScript standard calls the JavaScript version the execution context stack.
06 · RECURSION
A function can call any function, including itself. That sounds like a loop with no end, and it would be, except for one rule: every recursive function needs a base case, a problem small enough to answer directly, and a recursive case that hands a smaller problem to another copy of itself.
4! (four factorial) is 4 × 3 × 2 × 1. It is also 4 × 3!, which is a smaller factorial. So factorial(4) asks factorial(3), which asks factorial(2), down to factorial(1), which just answers 1. On the tower, four frames of the same function stack up, each with its own n, each waiting. Then the answers come back down: 1, 2, 6, 24.
Press Step through and count the frames in the "inside" line as it goes down and comes back up.
Each frame keeps its own n: that is the whole trick. Indenting by depth makes the stack visible in plain text:
Factorial is easier as a loop, honestly. Recursion shines when the data is recursive: folders inside folders, comments with replies, a web page's elements, the parse tree from chapter 1. The function mirrors the shape of the data:
Add a folder inside photos with a file in it. The function needs no change at all, however deep it goes.
07 · STACK OVERFLOW
The stack is a fixed slab of memory, set aside before the program runs. Forget the base case, and every call pushes another frame with no pop to come. The tower grows until it hits the limit, and the engine stops the program rather than letting it scribble over other memory. That is a stack overflow; in JavaScript it is a RangeError.
How tall can the tower get? It depends on the engine, the thread, and how big each frame is. Measure it:
Give dive some local variables (const a = 1, b = 2, c = 3;) and run it again: bigger frames, fewer of them fit.
Python is stricter on purpose: it counts frames and stops at 1,000 by default, long before the real stack runs out.
import sys
print(sys.getrecursionlimit()) # 1000 by default
def forever(n):
return forever(n + 1)
forever(0)
# RecursionError: maximum recursion depth exceeded
The site Stack Overflow is named after this error, which tells you how often programmers meet it. The usual causes: a missing base case, a base case that is never reached (say, n === 0 when n goes 3, 1, −1 …), or genuinely deep data handled recursively.
08 · TAIL CALLS
Look at what factorial's frame does after its call returns: it still has to multiply by n. So it must wait, and the frames pile up. Now move the multiplying into the arguments, carrying the answer so far in an accumulator. Then the recursive call is the very last thing the function does: a tail call. Nothing is left to do afterwards, so the frame could simply be reused.
Step through factorialTail: acc goes 1, 5, 20, 60, 120 on the way down, so the last call already holds the answer.
On the stage, one frame is rewritten in place: factorial(4, 1), (3, 4), (2, 12), (1, 24). The faint outlines are the frames an ordinary call would have stacked. Reusing the frame is called tail call optimisation; it makes a tail-recursive function run in constant stack space, exactly like a loop.
The catch: since ES2015 the JavaScript standard requires proper tail calls in strict mode, but only Safari's engine (JavaScriptCore) implements them. V8, in Chrome and Node, and Firefox's SpiderMonkey keep every frame. So here, this still overflows:
In JavaScript the dependable fix is to turn the tail call into a loop yourself (the parameters become variables you update), or to use a trampoline: return the next step as a function, and let a loop call it.
Other languages promise it. Scheme's standard requires tail calls to run in constant space, so Scheme programmers write loops as recursion. And an optimising C compiler often does it without being asked:
;; Scheme guarantees tail calls: this runs in constant space
(define (sum-to n acc)
(if (= n 0)
acc
(sum-to (- n 1) (+ acc n))))
(sum-to 1000000 0) ; => 500000500000, no overflow
long factorial_tail(long n, long acc) {
if (n <= 1) return acc;
return factorial_tail(n - 1, n * acc);
}
/* gcc -O2 turns this into a loop. Abridged output (x86-64):
factorial_tail:
mov rax, rsi ; acc
.L2:
imul rax, rdi ; acc = acc * n
... ; n = n - 1 (unrolled twice)
jne .L2 ; jump back: no call, no new frame
ret */
09 · THE BLOW-UP
The Fibonacci numbers go 0, 1, 1, 2, 3, 5, 8, 13 …: each is the sum of the two before. That definition turns straight into code with two recursive calls, and the calls form a tree instead of a tower. The stage draws every call fib(5) makes, in the order they happen. The lit path from the top to the cursor is the call stack at that moment; the tree is every stack the program ever had, laid side by side.
Add 30 to the list. Then 35. Feel the wait grow.
Count the boxes: f(3) is worked out twice, f(2) three times, f(1) five times. Every extra 1 on n multiplies the work by about 1.6 (the golden ratio), so the calls grow exponentially: 2·fib(n+1) − 1 of them.
The fix is to remember. A memo stores each answer the first time; after that, asking again costs one lookup. In the second half of the stage loop the repeated subtrees go dark and the gold boxes are memo hits: 9 calls instead of 15. For fib(70) it is 139 calls instead of about 600 trillion.
Or skip recursion: build up from the bottom with two variables. Same answer, constant memory.
10 · MAP, FILTER, REDUCE
Because functions are values, a function can take another function as an argument, or return one. A function that does either is a higher-order function. You have already met one: makeCounter returns a function. The three you will use most take one, and they replace most loops over lists:
map(fn): a new list with fn applied to every item.filter(fn): a new list of the items where fn says true.reduce(fn, start): combine all the items into one value, carrying a running result.On the conveyor: 1 to 6 go through map and come out squared, filter drops the odd ones off the side, and reduce pours what is left into one total, 4 + 16 + 36 = 56.
Change x % 2 === 0 to x > 10. Then make reduce multiply instead of add (what should the start value be?).
There is no magic inside them. map is a loop you write once and never again:
The same three ideas appear in every language, sometimes in disguise. Python prefers list comprehensions; SQL builds them into the language:
nums = [1, 2, 3, 4, 5, 6]
squares = [x * x for x in nums]
evens = [x for x in squares if x % 2 == 0]
total = sum(evens) # functools.reduce does the general case
print(squares, evens, total) # [1, 4, ..., 36] [4, 16, 36] 56
-- SQL says WHAT you want; map, filter and reduce are built in
SELECT SUM(x * x) -- map (x * x), then reduce (SUM)
FROM nums
WHERE (x * x) % 2 = 0; -- filter
-- 56
Because map treats every item separately, the items can be done in any order, or all at once. That is what a GPU does with pixels. In 2004 Google described MapReduce, a system that runs a map step and a reduce step over thousands of machines, named after these very functions (more in The Cluster).
11 · PURE VS SIDE EFFECTS
A pure function depends only on its arguments and does nothing but return a value. Same input, same output, every time, and nothing else in the world changes. addTax(100) is 125 today, tomorrow and on every computer.
Anything else a function does is a side effect: printing, changing a variable outside itself, writing a file, sending a request. Reading something hidden counts too: the clock, a random number, outside state. On the stage, the right-hand box is wired to all of them, and the same input, 100, gives 100, then 200, then 300.
Look at the order of the output: the three "total is now" lines appear before the line that called them, because each call printed while the arguments were being worked out.
Side effects aren't bad: they are the only reason to run a program (chapter 1 called them output). But pure functions are far easier to test, to reuse, to run in parallel and to cache (the memo trick only works on pure functions). So good programs keep a pure core, where the thinking happens, and push side effects out to a thin layer at the edge.
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.
Write toFahrenheit: it takes degrees Celsius and returns degrees Fahrenheit (multiply by 9, divide by 5, add 32). It should return, not print.
This should print 6, but prints undefined. Fix it with as small a change as you can.
Change makeCounter so it takes a step. makeCounter(5) should give a function that returns 5, then 10, then 15. Two counters must not share a count.
Write power(base, exp) without loops, ** or Math.pow. Use the fact that baseexp = base × baseexp−1, and that anything to the power 0 is 1. Then press Step through and watch the stack.
Run this: it times out, because fib(70) would take about 600 trillion calls. Add a memo so each fib(n) is worked out only once, and make it print 190392490709135.
Make total the sum of every price above 20, after adding 25% tax to each. Use filter, map and reduce, no loops. The answer is 217.5.
addBonus changes the caller's array, which surprised whoever wrote the last line. Rewrite it so it returns a new array and leaves its argument alone.
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 is the difference between a parameter and an argument?
What does a JavaScript function return if it has no return statement?
Why can an inner function read its outer function's variables, but not the other way round?
makeCounter has returned, so why does count still exist?
What is in a stack frame, and when does it go away?
Why does fib(40) written the obvious way take 331 million calls, and what is the fix?
What makes a call a tail call, and does Chrome optimise it?
GO FURTHER
SOURCES