17.0 What this chapter gives you#
- You will be able to say what a programming language actually is, and why there are thousands of them instead of one.
- You will be able to read a small program in six different languages and see that they are all saying the same thing.
- You will be able to explain what a type is, why static and dynamic typing argue with each other, and what the billion dollar mistake was.
- You will be able to describe the three ways a program can manage memory, and name the exact failure each one prevents and each one allows.
- You will be able to name the main programming paradigms and write the same task in two of them.
- You will be able to pick the right data structure for a job and state its cost in Big-O notation without looking it up.
- You will be able to explain why concurrency is genuinely hard, and show a race condition line by line.
- You will be able to write a test, read someone else’s code, and judge a dependency before you install it.
- You will be able to choose a language for a real project and defend the choice with reasons instead of taste.
17.1 What a programming language is#
PLAIN17.1.1 in simple words#
- A programming language is a way of writing down instructions that both a person and a machine can work with.
- It has a grammar: rules about which arrangements of symbols are legal.
- It has a meaning: rules about what each legal arrangement does.
- Those two things are separate. A sentence can be perfectly formed and still say something silly or wrong.
- The grammar is called syntax. The meaning is called semantics.
- A language is designed for humans first. Machines only understand numbers, and something else turns your text into those numbers.
- That something else is a compiler or an interpreter. Chapter 16 covered how a compiler works. Here we care about the language itself.
- A language is not the same as its tools. C is a written standard. GCC and Clang are two programs that implement it.
PLAIN17.1.2 a picture in your head#
- Think of a knitting pattern.
- A pattern has a strict notation:
k2, p2, rep to end. Every symbol means one exact action.
- The notation has a grammar.
k2 is legal. k on its own with no number is not, because the pattern must say how many.
- The notation also has a meaning.
k2 means knit two stitches. Follow it and a real object appears.
- Two knitters in different countries use different pattern notations. Both make the same jumper. The garment is the program, the notation is the language.
- A pattern can be written correctly and still produce a jumper with three sleeves. The notation was obeyed. The intention was wrong.
Where this comparison breaks: a knitting pattern is followed by a patient human who silently fixes obvious mistakes. A computer fixes nothing. It does exactly what you wrote, at billions of steps per second, including the wrong thing. A pattern also has no way to say “repeat until the wool runs out and then decide what to do next”. Real languages do, and that ability to make decisions is what separates a program from a recipe.
PLAIN17.1.3 a worked example#
- Here are three lines of Python. Each is broken in a different way.
print("hello"
x = 10 / 0
average = total / count
- Line 1 is a syntax error. The closing bracket is missing. The grammar was violated. Python refuses to run the file at all.
- Line 2 is legal grammar but impossible meaning. Division by zero. Python starts running, reaches this line, and stops with an error.
- Line 3 has perfect grammar and perfect meaning. It will run happily. But if
count is the number of rows in the file and total is the sum of a different column, the answer is nonsense and nothing complains.
- That third case is the one that costs money. No tool caught it because nothing was broken. The program did exactly what you said.
- This is the whole of programming in one example: getting the syntax right is easy and the machine helps you. Getting the meaning right is hard and you are mostly on your own.
PLAIN17.1.4 what is really happening inside#
- When you save a source file you have saved plain text. Bytes. Nothing more.
- A tool reads that text left to right and chops it into tokens: names, numbers, symbols, keywords.
- It then checks whether those tokens form a legal shape, using the grammar. This is parsing, and it builds a tree of the program’s structure.
- If the shape is illegal you get a syntax error, with a line number.
- If the shape is legal, the tool works out what it means, and produces either machine instructions or a set of actions it will perform itself.
- Because a language is a written definition and not a program, several different tools can implement it. Python has CPython, PyPy and others.
- This is why “the language is slow” is usually the wrong sentence. The implementation is slow. The language is a document.
- Why thousands of languages exist: every language is a set of trade-offs. Fast to write against fast to run. Safe against flexible. Small against expressive. Nobody has found one point that wins everywhere.
- A new language is also cheap to start and expensive to finish. Thousands are started. About twenty are widely used at any moment.
TECHNICAL17.1.5 the engineer’s version#
- Syntax is normally specified as a context-free grammar in Backus-Naur Form. John Backus described the notation in 1959 for ALGOL 58, and Peter Naur refined it in the ALGOL 60 report of 1960, which is why it carries both names.
- Noam Chomsky published the hierarchy of formal grammars in 1956. Type 3 is regular (what a lexer handles), type 2 is context-free (what a parser handles), type 1 is context-sensitive, type 0 is unrestricted.
- Real languages are not purely context-free. C requires a symbol table to decide whether
A * B; is a multiplication or a pointer declaration. This is called the lexer hack, and it is an implementation detail, not a standard.
- Semantics has three classical formal styles: operational (Gordon Plotkin’s structural operational semantics, 1981), denotational (Dana Scott and Christopher Strachey, around 1970), and axiomatic (Tony Hoare, in the October 1969 paper “An Axiomatic Basis for Computer Programming”).
- In practice most language specifications define semantics in careful English prose, not in formal mathematics. Standard ML and WebAssembly are among the very few with a fully formal specification.
- Undefined behaviour is a specification concept, not a bug: the standard declines to say what happens, so the compiler may assume it never occurs. Signed integer overflow in C is the classic case.
| C |
ISO/IEC 9899 |
C23, published 2024 |
| C++ |
ISO/IEC 14882 |
C++26, March 2026 |
| JavaScript |
ECMA-262 |
ECMAScript 2025 |
| SQL |
ISO/IEC 9075 |
SQL:2023 |
| Python |
PEPs, no ISO standard |
3.14, Oct 2025 |
- There is no census of languages. The Online Historical Encyclopaedia of Programming Languages lists roughly 9,000 entries, which is an approximate figure and includes long-dead research languages.
- Tools that show you the grammar in action:
python -m ast, gcc -fdump-tree- original, clang -Xclang -ast-dump, and node --print-ast style flags in various runtimes.
WORDS17.1.6 remember these#
- Syntax — the spelling and shape rules — the context-free grammar of the language.
- Semantics — what it means when it runs — the operational or denotational definition of program behaviour.
- Token — one word or symbol — the smallest unit produced by lexical analysis.
- Parsing — checking the shape — building an abstract syntax tree from tokens according to the grammar.
- Specification — the official rulebook — the ISO, ECMA or community document that defines the language, separate from any implementation.
- Undefined behaviour — the rules do not say — a construct the standard leaves unconstrained, allowing any compiler output at all.
17.2 The building blocks every language has#
PLAIN17.2.1 in simple words#
- Almost every language is built from the same ten or so ideas.
- A variable is a named box that holds a value you can change.
- A literal is a value written down directly, like
42 or "cat".
- An operator is a symbol that combines values, like
+ or <.
- An expression is anything that produces a value:
2 + 3, name, total > 100.
- A statement is a complete instruction that does something: assign, print, return.
- A conditional chooses between paths: if this, do that, otherwise do the other.
- A loop repeats a block, either a fixed number of times or until some test stops being true.
- A function is a named piece of program you can call from elsewhere, optionally handing it values and getting one back.
- Scope is the region of the program where a name is visible.
- A comment is text the machine ignores completely, written for humans.
PLAIN17.2.2 a picture in your head#
- Think of a kitchen with labelled jars on a shelf.
- A jar is a variable. The label is the name, the contents are the value.
- A literal is an ingredient you bring in fresh, not from a jar.
- An operator is a kitchen action: mix these two, compare these two.
- A recipe step that produces something you can hold is an expression. A step that just says “turn on the oven” is a statement.
- A conditional is “if the dough is sticky, add flour”.
- A loop is “stir for ten minutes”.
- A function is a sub-recipe on its own card: “make the sauce”. You can use it from any recipe without rewriting it.
- Scope is which shelf you can reach from where you are standing. Jars in the locked pantry are out of scope.
Where this comparison breaks: a cook can ignore a step or improvise. A program cannot. Also, a real recipe never calls itself, and functions often do, which is called recursion and has no clean kitchen equivalent.
PLAIN17.2.3 a worked example#
- Here is the same tiny program in six languages. It adds the numbers 1 to 10 and prints 55.
- Read them one after another. Notice that the punctuation changes and nothing else does.
#include <stdio.h>
int main(void) {
int total = 0;
for (int i = 1; i <= 10; i++) {
total = total + i;
}
printf("%d\n", total);
return 0;
}
total = 0
for i in range(1, 11):
total = total + i
print(total)
let total = 0;
for (let i = 1; i <= 10; i++) {
total = total + i;
}
console.log(total);
public class Sum {
public static void main(String[] args) {
int total = 0;
for (int i = 1; i <= 10; i++) {
total = total + i;
}
System.out.println(total);
}
}
package main
import "fmt"
func main() {
total := 0
for i := 1; i <= 10; i++ {
total = total + i
}
fmt.Println(total)
}
fn main() {
let mut total = 0;
for i in 1..=10 {
total = total + i;
}
println!("{}", total);
}
- Every one of them declares a variable called
total, sets it to zero, loops i from 1 to 10, adds i to total, and prints the result.
- The differences are: whether you write the type (
int) or not, whether lines end in a semicolon, whether blocks use braces or indentation, and how much ceremony wraps the main body.
- Java has the most ceremony because every piece of code must live inside a class. Python has the least because the file itself is the program.
- Rust needs
mut because in Rust a variable cannot be changed unless you say so explicitly.
- Go uses
:= to mean “make a new variable and work out its type for me”.
- If you can read one of these, you can read all six with about an hour of effort. That is the real point of this section.
PLAIN17.2.4 what is really happening inside#
- A variable is a name the compiler maps to a place: a CPU register, a slot on the stack, or an address in the heap.
- In
total = total + i, the machine loads two values, adds them in a register, and stores the result back. Three steps, one line of your text.
i++ is one instruction on most processors. total = total + i is usually one instruction too, once the values are in registers.
- A loop is a conditional jump. At the bottom of the block, compare
i with 10, and if the test passes, jump back to the top.
- A function call pushes the return address and arguments somewhere the callee can find them, jumps to the function’s code, and later jumps back.
- Scope is a compile-time idea, not a runtime one. The compiler simply refuses to resolve a name that is not visible, and produces an error.
- Comments never reach the machine. The lexer throws them away before the parser ever sees them.
- Here is what the loop becomes in spirit:
total <- 0
i <- 1
top:
if i > 10 goto done
total <- total + i
i <- i + 1
goto top
done:
print total
- Every one of the six languages above compiles or interprets down to something with this shape. The high-level
for is a convenience.
TECHNICAL17.2.5 the engineer’s version#
- Expressions have precedence and associativity, fixed by the grammar. In C,
a + b * c parses as a + (b * c) because * binds tighter. a - b - c parses as (a - b) - c because - is left-associative.
- Statement versus expression is a real language design axis. In Rust and in most functional languages,
if is an expression and returns a value. In C and Java it is a statement and returns nothing, which is why those languages need a separate ternary operator ? :.
- Scope rules split into lexical (static) and dynamic. Almost every modern language uses lexical scope: a name resolves by where it is written. Emacs Lisp and Bash use dynamic scope in places, where a name resolves by who called you.
- Variable lifetime is separate from scope. A C
static local has function scope but program lifetime.
- Parameter passing conventions: call by value (C, Java for primitives, Go), call by reference (C++ with
&, C# with ref), and call by sharing (Python, JavaScript, Java for objects, where the reference is copied but the object is not).
- This is the source of a common confusion. In Python, reassigning a parameter inside a function does not affect the caller, but calling
list.append on it does. Both are consistent with call by sharing.
| Block delimiter |
braces |
indentation |
braces |
| Statement end |
semicolon |
newline |
semicolon |
| Declare variable |
int x = 1; |
x = 1 |
let x = 1; |
| Mutable by default |
yes |
yes |
no |
- Tools:
python -m dis shows the bytecode a Python function becomes. godbolt.org style compiler explorers show the assembly for C, C++, Rust and Go. javap -c disassembles a Java class file.
WORDS17.2.6 remember these#
- Variable — a named box for a value — a binding from an identifier to a storage location with a scope and lifetime.
- Literal — a value written directly — a constant token whose value is fixed at compile time.
- Expression — something that produces a value — a grammar production that evaluates to a value of some type.
- Statement — an instruction that acts — a grammar production executed for its effect rather than its value.
- Scope — where a name can be seen — the lexical region in which an identifier binding is visible.
- Call by sharing — the object is shared, the variable is not — the argument reference is copied, so mutation is visible and rebinding is not.
17.3 Types#
PLAIN17.3.1 in simple words#
- A type is the answer to “what kind of thing is this value”.
42 is a whole number. "42" is a piece of text. 4.2 is a decimal number. They look similar and behave completely differently.
- Types exist to stop you doing meaningless things, like subtracting a name from a date.
- They also tell the machine how many bytes to set aside and which instructions to use. Adding two whole numbers uses different hardware from adding two decimals.
- Some languages check types before the program runs. That is static typing.
- Some languages check types while the program runs. That is dynamic typing.
- Some languages refuse to mix types silently. That is strong typing.
- Some languages quietly convert one type into another to make an expression work. That is weak typing.
- Static and strong are different questions. A language can be any of the four combinations.
PLAIN17.3.2 a picture in your head#
- Think of an airport with two kinds of security.
- Static typing is the check at the gate before boarding. Nobody without a valid ticket gets on the plane at all.
- Dynamic typing is the check by the cabin crew mid-flight. Most people are fine, but if someone is in the wrong seat you find out at 30,000 feet.
- Strong typing is a strict officer who says a bus ticket is not a plane ticket, full stop.
- Weak typing is an officer who looks at your bus ticket, decides it is probably fine, and lets you through.
- Static checking catches problems earlier and cheaper. Dynamic checking lets you board faster and change plans later.
Where this comparison breaks: static checks are not free and they are not complete. A type checker proves that the shapes fit together. It does not prove your program is correct. A program that computes the wrong average with perfectly matched types passes every static check ever written.
PLAIN17.3.3 a worked example#
- Watch the same expression in three languages.
Expression: 1 + "2"
Python -> TypeError, refuses to run this line
JavaScript -> "12" (number turned into text)
PHP -> 3 (text turned into number, PHP 8+)
- Python is dynamically typed and strongly typed. It waits until runtime to check, and then refuses to guess.
- JavaScript is dynamically typed and weakly typed. It converts the number to text and joins them.
- PHP is dynamically typed and weakly typed in the other direction for
+, because + in PHP is only arithmetic and . is the joining operator.
- Now the static side. In Java,
int x = "hello"; will not compile. The error arrives in under a second, at your desk, before anyone else sees the code.
- In Python,
x = "hello" followed a hundred lines later by x + 1 will run fine until it reaches that line, possibly in production at 3am.
- Type inference means the compiler works out the type so you do not have to write it. In Rust,
let x = 5; gives an i32 without you saying so. This is still static typing. The type is fixed, it just was not typed by hand.
PLAIN17.3.4 what is really happening inside#
- A type is mostly a compile-time idea. After compilation, memory holds bytes and nothing else.
- The type told the compiler which machine instruction to emit.
ADD for integers, ADDSD for double-precision decimals on x86.
- In a dynamic language, every value carries a tag at runtime saying what it is. That tag costs memory and a check on every operation.
- That is why a Python integer needs about 28 bytes while a C integer needs 4. The extra bytes hold the type pointer, a reference count and the digits.
- Composite types build bigger shapes from smaller ones: a struct or record groups named fields, an array groups values of one type, a tuple groups a fixed number of possibly different types.
- Generics let you write one piece of code that works for many types. A list of integers and a list of strings share one implementation. Without generics you either copy the code or throw away the type information.
- Null is a special value meaning “there is nothing here”. It is allowed where a real value is expected, which is exactly the problem.
- The fix is an option type: a value that is explicitly either “something” or “nothing”, and the language will not let you use it without checking which.
TECHNICAL17.3.5 the engineer’s version#
- Sizes. The C standard guarantees only minimum widths, so the exact sizes below are the near-universal LP64 arrangement used by Linux and macOS on x86-64 and ARM64. Windows uses LLP64, where
long is 4 bytes.
| int8, signed char |
1 byte |
-128 to 127 |
| uint8, unsigned char |
1 byte |
0 to 255 |
| int16, short |
2 bytes |
-32,768 to 32,767 |
| int32, int |
4 bytes |
-2,147,483,648 up |
| int64, long (LP64) |
8 bytes |
about -9.2e18 to 9.2e18 |
| float, binary32 |
4 bytes |
about 7 decimal digits |
| double, binary64 |
8 bytes |
about 15 to 17 digits |
| bool |
1 byte |
true or false |
| Rust char |
4 bytes |
one Unicode scalar value |
- Floating point follows IEEE 754, first published in 1985 and revised in 2008 and 2019. binary32 has 24 bits of significand, binary64 has 53. This is why
0.1 + 0.2 is 0.30000000000000004 in every language that uses binary64, including Python, JavaScript and Java.
- JavaScript numbers are all binary64. Integers are exact only up to 2^53 - 1, which is 9,007,199,254,740,991, exposed as
Number.MAX_SAFE_INTEGER. BigInt was added in ECMAScript 2020 for larger values.
- Type system taxonomy: static or dynamic (when checking happens), strong or weak (how much implicit conversion is allowed), nominal or structural (do two types match by name or by shape). Go interfaces and TypeScript are structural. Java classes are nominal.
- Type inference in ML-family languages uses Hindley-Milner, published by Roger Hindley in 1969 and independently by Robin Milner in 1978. Rust and Swift use local inference, which is weaker but gives better error messages.
- Generics are implemented either by monomorphization (Rust, C++ templates, C# value types), which duplicates code per type and is fast, or by erasure (Java, which added generics in Java 5 in 2004), which keeps one copy and loses the type at runtime.
- Null. Tony Hoare introduced the null reference in 1965 while designing the type system of ALGOL W. At a software conference in 2009 he called it “my billion-dollar mistake”, saying it had led to “innumerable errors, vulnerabilities and system crashes”.
- Option types are the fix:
Option<T> in Rust, Optional<T> in Java 8 and Swift, Maybe a in Haskell, T? with null-safety checks in Kotlin, and strictNullChecks in TypeScript since version 2.0 in 2016.
fn find(name: &str) -> Option<u32> {
if name == "ada" { Some(1815) } else { None }
}
match find("ada") {
Some(year) => println!("born {}", year),
None => println!("not found"),
}
- The compiler will not let you reach
year without handling None. That is the entire trick: the checking is not optional and not forgettable.
- Tools:
mypy and pyright add static checking to Python. tsc does it for JavaScript via TypeScript. sorbet does it for Ruby.
WORDS17.3.6 remember these#
- Type — what kind of value it is — a set of values plus the operations legal on them.
- Static typing — checked before running — type checking performed at compile time by the language’s type system.
- Dynamic typing — checked while running — types carried on values and verified at each operation.
- Strong typing — no silent conversions — the language rejects operations between incompatible types rather than coercing.
- Type inference — the compiler guesses correctly — deducing types from context without explicit annotations, still statically.
- Generics — one implementation for many types — parametric polymorphism, realized by monomorphization or by erasure.
- Option type — a value that may be absent, safely — a sum type with a present and an absent case, forcing the caller to handle both.
17.4 Memory management#
PLAIN17.4.1 in simple words#
- A running program needs space to keep things. That space is memory.
- Some space is easy. A number you use inside one function lives on the stack, and disappears by itself when the function ends.
- Some space is hard. Anything whose size you only learn while running, or that must outlive the function that made it, lives on the heap.
- Heap space has to be asked for, and later given back.
- There are exactly three answers to “who gives it back”.
- Answer one: you do, by hand. This is C and old C++. It is fast and it is easy to get wrong.
- Answer two: the language does, automatically, by watching your values while the program runs. This is Java, C#, Go, Python, JavaScript and most others.
- Answer three: the compiler works it out before the program runs, from rules you followed while writing. This is Rust.
- Every one of the three is a real trade. None of them is free.
PLAIN17.4.2 a picture in your head#
- Think of a library with a room of borrowed books.
- Manual memory management is a library with no due dates. You borrow a book and you must remember to return it. If you forget, the room fills up. If you return the same book twice, the desk gets confused. If you return a book and then keep reading it, you are reading a book that has been given to someone else.
- Reference counting is a library where each book has a tally on the cover. Add one when someone borrows it, subtract one when they finish. At zero, it goes back on the shelf immediately.
- Tracing garbage collection is a librarian who periodically walks the whole building, notes every book anyone is actually holding, and reshelves the rest. While she walks, nobody may move.
- Rust’s ownership is a library rule that each book has exactly one owner, and the owner must return it when they leave the room. The rule is checked at the door, before you enter.
Where this comparison breaks: books are visible and countable, and a librarian can see who is holding one. Real memory has no such property. The collector can only follow pointers from a known starting set, and anything not reachable that way is assumed dead, even if you meant to keep it.
PLAIN17.4.3 a worked example#
- Here is manual allocation in C, done correctly.
#include <stdlib.h>
int *make_array(int n) {
int *a = malloc(n * sizeof(int));
if (a == NULL) return NULL;
for (int i = 0; i < n; i++) a[i] = i;
return a;
}
/* caller must eventually call free(a) */
malloc asks for a block of bytes and hands back its address. free gives it back. Nothing else does.
- Now the four ways this goes wrong.
- Leak: you never call
free. The block stays reserved for the life of the process. Do it in a loop and memory grows until the process is killed.
- Double free: you call
free(a) twice. The allocator’s internal list is corrupted. This is a classic route to a security exploit.
- Use after free: you call
free(a) and then read a[0]. The bytes may still look right for a while, which makes this bug appear and disappear at random.
- Dangling pointer: you return the address of a local variable, which lived on the stack and vanished the moment the function ended.
int *bad(void) {
int x = 42;
return &x; /* x dies here; the pointer is dangling */
}
- All four compile without error in plain C. Three of the four are silent during testing and loud in production.
PLAIN17.4.4 what is really happening inside#
- Reference counting. Each object carries a small number. Every time a new name points at it, add one. Every time a name stops pointing at it, subtract one. At zero, free it at once.
- That is simple, immediate and predictable. It has two costs. Every assignment does extra arithmetic, and in a multi-threaded program that arithmetic must be atomic, which is slow.
- It also cannot free a cycle. If A points at B and B points at A, both counts stay at one forever even though nothing outside can reach them.
- Tracing garbage collection solves the cycle problem by asking a different question: not “how many people point at this” but “can I still reach this”.
- Mark and sweep works in two phases. Start from the roots (global variables, stack slots, registers). Follow every pointer and mark everything you can reach. Then walk the whole heap and free everything unmarked.
- Most objects die very young. Measured across many programs, the large majority of allocations become garbage almost immediately. This observation is called the generational hypothesis.
- So a generational collector splits the heap. New objects go in a small young area, collected often and quickly. Objects that survive a few collections are promoted to an old area, collected rarely.
- A GC pause is the time the program is stopped so the collector can work without objects moving under its feet. Modern collectors do most of the work concurrently and stop the world only briefly.
- Rust’s answer removes the question. Every value has exactly one owner. When the owner goes out of scope, the value is freed. You can lend a value out as a borrow, and the compiler checks that no borrow outlives the owner.
- There is no runtime cost at all, because all of this is checked before the program runs. The price is that the compiler rejects programs a human can see are fine, and you must restructure them.
fn main() {
let s = String::from("hello");
let r = &s; // borrow, does not take ownership
println!("{} {}", s, r); // both usable
} // s dropped here, memory freed
TECHNICAL17.4.5 the engineer’s version#
malloc and free are C standard library functions, not system calls. They sit on top of brk, sbrk or mmap. glibc uses ptmalloc2; alternatives include jemalloc, tcmalloc and mimalloc, chosen for different fragmentation and threading behaviour.
- Fragmentation is the memory management failure nobody warns you about: the allocator holds plenty of free bytes, but no single contiguous run large enough for the next request.
- Reference counting in CPython is the primary mechanism, with a separate cycle detector for the case counting cannot handle. Swift uses ARC, automatic reference counting, inserted by the compiler at compile time, and requires the programmer to mark cycles with
weak or unowned.
- Tracing collectors in production, with real characteristics:
| G1 (default since Java 9) |
JVM |
tens of milliseconds |
| ZGC (production, Java 15) |
JVM |
under 1 ms |
| Shenandoah |
JVM |
under 10 ms |
| Go’s collector |
Go |
under 1 ms since Go 1.8 |
- Go’s collector is concurrent, tri-colour and non-moving. The Go 1.5 release in August 2015 was the point where sub-10 ms pauses became normal, and Go 1.8 in 2017 removed the stop-the-world stack rescanning that had been the main remaining source of long pauses.
- ZGC and Shenandoah are concurrent compacting collectors using load or store barriers, so pause time is largely independent of heap size. This is the headline claim, and it holds for pause length, not for total throughput cost.
- Rust’s rules, stated exactly: each value has one owner; there may be either any number of immutable borrows or exactly one mutable borrow at a time, not both; every borrow’s lifetime must be contained within the owner’s. The checker that enforces this is the borrow checker, rewritten as NLL (non-lexical lifetimes) in the Rust 2018 edition.
- Escape hatches exist and are honest:
Rc<T> and Arc<T> add reference counting when single ownership does not fit, and unsafe blocks let you do what C does, in clearly marked regions.
| Manual (C) |
fastest possible |
none, all bugs live |
fully predictable |
| Ref counting |
steady small cost |
leaks cycles |
very predictable |
| Tracing GC |
fast alloc, pauses |
memory-safe |
unpredictable |
| Ownership (Rust) |
same as manual |
memory-safe |
fully predictable |
- The honest version: “Rust has no runtime cost” is nearly true but not entirely. Bounds checks on slice indexing remain at runtime unless the optimizer can prove them redundant, and
Rc/Arc cost the same as any other reference count.
- Microsoft reported in 2019 that around 70 percent of the security vulnerabilities it assigned CVEs to were memory safety issues. The Chromium project has published a very similar figure. This is the single strongest argument for the second and third approaches.
- Tools:
valgrind --leak-check=full, AddressSanitizer (-fsanitize=address), heaptrack, jcmd GC.heap_info, GODEBUG=gctrace=1, and tracemalloc in Python.
WORDS17.4.6 remember these#
- Stack — automatic space that cleans itself — a contiguous region managed by push and pop, with frame lifetime tied to call depth.
- Heap — space you ask for and give back — a dynamically managed region served by an allocator with explicit or automatic reclamation.
- Memory leak — space never given back — allocated memory that remains reachable-but-unused or unreachable-but-unfreed for the process lifetime.
- Use after free — reading a returned book — dereferencing a pointer to memory already released, a leading cause of exploitable vulnerabilities.
- Reference counting — keep a tally per object — incrementing and decrementing a per-object counter, freeing at zero, unable to reclaim cycles.
- Mark and sweep — find what is reachable, free the rest — a tracing algorithm that marks from roots then sweeps unmarked objects.
- GC pause — the moment everything stops — a stop-the-world phase during which mutator threads are suspended for collector work.
- Ownership — one owner, checked at compile time — Rust’s affine type discipline enforced by the borrow checker with zero runtime cost.
17.5 Paradigms#
PLAIN17.5.1 in simple words#
- A paradigm is a style of organizing a program. It is a habit of thought, not a feature.
- Imperative means you write a sequence of commands that change state. Do this, then this, then this.
- Procedural is imperative plus the idea of grouping commands into named procedures you can call.
- Object-oriented groups data together with the operations on that data into objects.
- Functional treats computation as evaluating functions, avoids changing things in place, and passes functions around as ordinary values.
- Declarative means you describe what you want, not how to get it. SQL and HTML are declarative.
- Event-driven means the program sits waiting and reacts when something happens: a click, a message, a timer.
- Most real languages support several of these. The paradigm is a choice you make per program, and sometimes per file.
PLAIN17.5.2 a picture in your head#
- Imagine you want a cup of tea and there are five ways to get one.
- Imperative: you write out every step yourself. Fill kettle, switch on, wait, pour, add bag, wait three minutes, remove bag.
- Procedural: you write “boil water”, “brew”, “serve” as three named jobs, then call them in order.
- Object-oriented: you build a Kettle that knows how to boil itself and a Cup that knows how to be filled. You tell the kettle to boil, not the water.
- Functional: you write
serve(brew(boil(water))). Each step takes something and returns a new something, and nothing is modified in place.
- Declarative: you write “I want tea, medium strength, with milk” and something else works out the steps.
- Event-driven: you sit down, and when the kettle clicks off, you react.
Where this comparison breaks: real programs mix all of these in one file, and the boundaries are much blurrier than five clean styles suggest. Also the declarative version only exists because somebody wrote the imperative version underneath it. Declarative never removes the work, it moves it.
PLAIN17.5.3 a worked example#
- The task: given a list of orders, find the total value of the ones over 100.
- First in an object-oriented style, in Python.
class OrderBook:
def __init__(self):
self.orders = []
def add(self, value):
self.orders.append(value)
def total_over(self, limit):
total = 0
for v in self.orders:
if v > limit:
total += v
return total
book = OrderBook()
for v in [50, 120, 300, 80]:
book.add(v)
print(book.total_over(100)) # 420
- Now the same task in a functional style.
from functools import reduce
orders = [50, 120, 300, 80]
big = filter(lambda v: v > 100, orders)
total = reduce(lambda a, b: a + b, big, 0)
print(total) # 420
- Both print 420. The difference is where the state lives.
- In the first,
self.orders is a thing that exists, holds state, and is changed by add. Behaviour is attached to that thing.
- In the second there is no thing. There is a list going in and a number coming out, through two functions that each produce a new value.
- The functional version is easier to test, because there is nothing to set up. The object version is easier to extend when you later need customer names, dates and discounts hanging off each order.
- Neither is better. They fail differently.
PLAIN17.5.4 what is really happening inside#
- Object-oriented programming rests on three ideas.
- Encapsulation: the data inside an object is private, and you touch it only through the object’s own methods. That way the object can guarantee its own rules stay true.
- Inheritance: a new class can be defined as “an existing class, plus these changes”. A
SavingsAccount is an Account with an interest rate.
- Polymorphism: many different objects can respond to the same message in their own way. Call
area() on a Circle and a Square and each does the right thing without the caller knowing which it holds.
- The honest criticism of inheritance: deep inheritance chains are one of the most reliable ways to make a codebase hard to change.
- When class E inherits from D inherits from C inherits from B inherits from A, reading one method means reading five files, and changing A silently changes the behaviour of dozens of classes you have never opened.
- This is why the modern advice, common since the 1994 book Design Patterns by Gamma, Helm, Johnson and Vlissides, is “favour composition over inheritance”: hold an object rather than inherit from it.
- Go took this seriously and has no inheritance at all, only embedding and interfaces. Rust has traits and no class inheritance. Both are widely used, which shows inheritance is not required for large software.
- Functional programming rests on different ideas.
- A pure function returns the same answer for the same inputs and changes nothing outside itself. It cannot write a file, print, or read the clock.
- Immutability means values are never changed in place. To “change” a list you produce a new list. Nothing you already handed to someone else can shift under them.
- A higher-order function takes a function as an argument or returns one.
map, filter and reduce are the three you meet constantly.
map applies a function to every item and gives a new sequence. filter keeps the items passing a test. reduce folds a sequence down to one value by combining two at a time.
TECHNICAL17.5.5 the engineer’s version#
- Timeline. Fortran, 1957, John Backus at IBM: imperative. Lisp, 1958, John McCarthy at MIT: the first functional language. Simula 67, 1967, Ole-Johan Dahl and Kristen Nygaard in Norway: the first object-oriented language, and the source of the word “class”. Smalltalk, 1972 at Xerox PARC, Alan Kay and colleagues: the language that defined object-oriented as a whole worldview.
- Alan Kay, who coined the term “object-oriented”, said later that he meant messaging between independent objects rather than classes and inheritance, and that C++ and Java were not what he had in mind. Experts genuinely disagree about which reading is the useful one.
- SOLID, named by Michael Feathers around 2004 from principles Robert Martin collected, is the standard object-oriented design checklist: single responsibility, open-closed, Liskov substitution, interface segregation, dependency inversion. Barbara Liskov stated the substitution principle in a 1987 keynote.
- Substitution in practice: if
Square inherits from Rectangle and Rectangle has setWidth and setHeight, a Square cannot honour both independently. This is the classic demonstration that “is a” in English is not “is a” in a type system.
- Functional core concepts with names: referential transparency (an expression can be replaced by its value without changing behaviour), persistent data structures (updates share most of the old structure, so a “copy” is cheap), currying, closures, and algebraic data types.
- Persistent data structures are what make immutability affordable. Clojure’s vectors use a 32-way branching trie, so an update copies about five small nodes rather than the whole vector.
- Functional ideas have won in practice even where the paradigm has not.
map and filter are in every mainstream language. Java 8 added lambdas and streams in March 2014. C++11 added lambdas in 2011. Python has had lambda since 1994.
| Procedural |
the procedure |
C, 1972 |
| Object-oriented |
the object |
Smalltalk, 1972 |
| Functional |
the pure function |
Haskell, 1990 |
| Declarative |
the description |
SQL, 1974 |
| Logic |
the rule |
Prolog, 1972 |
- Event-driven is a control-flow architecture rather than a paradigm in the same sense. Its engine is a loop that pulls from a queue and dispatches to handlers, which we take apart in section 17.8.
WORDS17.5.6 remember these#
- Paradigm — a style of organizing code — a coherent model of computation and program structure.
- Encapsulation — keep the insides private — restricting direct access to state so invariants are enforced by the type’s own operations.
- Inheritance — a class built on another — subtyping with implementation reuse from a parent class.
- Polymorphism — one call, many behaviours — dispatch of an operation based on the runtime or static type of its operand.
- Pure function — same input, same output, no side effects — referentially transparent, with no observable interaction outside its return value.
- Immutability — never change, always replace — values that cannot be modified after construction, usually with structural sharing for efficiency.
- Higher-order function — a function taking or returning a function — a function whose domain or codomain includes function types.
17.6 Data structures, properly#
PLAIN17.6.1 in simple words#
- A data structure is a way of arranging values in memory so that certain operations are cheap.
- Every structure is good at some things and bad at others. There is no best one.
- An array is a fixed row of slots, all the same size, side by side. You can jump straight to slot 500.
- A dynamic array is an array that grows when it fills up, by making a bigger one and copying.
- A linked list is a chain: each item holds a value and the address of the next item. Items can be anywhere in memory.
- A stack is last in, first out. Like a pile of plates.
- A queue is first in, first out. Like a queue at a counter.
- A deque is a queue you can push to and pop from at both ends.
- A hash table maps a key to a value with near-instant lookup.
- A set is a collection with no duplicates and fast “is this in here”.
- A binary search tree keeps items in order so you can find, insert and delete in a small number of steps.
- A heap keeps the smallest (or largest) item instantly available.
- A graph is nodes joined by edges. Roads, friendships, dependencies.
- A trie stores strings by shared prefix, so all words starting “car” sit under one path.
PLAIN17.6.2 a picture in your head#
- Think of ways to store books in a house.
- An array is a shelf of identical slots, numbered. You want book 40, you walk straight to slot 40. But inserting a book in the middle means shifting every book after it.
- A linked list is a treasure hunt. Each book has a note saying where the next one is. Inserting is trivial, you just rewrite two notes. Finding book 40 means following 39 notes.
- A hash table is a set of drawers labelled A to Z, where a rule turns the title into a drawer number. You compute the drawer and open it. If two books land in the same drawer, you keep a small pile there.
- A binary search tree is a house where each room says “smaller titles that way, larger titles that way”. Twenty questions gets you to any of a million books.
- A heap is a pile where the lightest book is always on top, and nothing else is sorted at all.
Where this comparison breaks: real memory has a property no shelf has, called locality. Reading one byte pulls its neighbours into cache for free. So an array is far faster than its step count suggests, and a linked list far slower, even when the theory says they are equal. Measured on modern hardware, scanning an array can be several times faster than walking a linked list of the same length.
PLAIN17.6.3 a worked example#
- Take a hash table holding names against phone numbers.
- You insert
"ada" -> 5551234.
- The hash function turns the text “ada” into a number. Suppose it gives 3,481,092,776.
- The table has 8 buckets. Take the hash modulo 8: 3,481,092,776 mod 8 = 0.
- So “ada” goes into bucket 0, storing both the key and the value.
- Now insert
"bob" -> 5559876. Suppose hash("bob") mod 8 is also 0.
- That is a collision. Two different keys, one bucket.
- Handling it by chaining: bucket 0 holds a tiny list, and now that list has two entries. Looking up “bob” finds bucket 0, then compares keys within it.
- Handling it by open addressing: if bucket 0 is taken, try bucket 1, then 2, until an empty one is found. Lookup follows the same walk.
buckets, 8 slots, chaining:
0 -> ["ada" 5551234] -> ["bob" 5559876]
1 -> empty
2 -> empty
3 -> ["cy" 5550000]
...
7 -> empty
- As the table fills, collisions get more common and lookups slow down.
- So the table watches its load factor: entries divided by buckets. When that crosses a threshold, typically 0.7 to 0.9, it allocates a bigger table and rehashes everything into it.
- That one resize is expensive, but it happens rarely, so the average cost of an insert stays constant. This is called amortized constant time.
PLAIN17.6.4 what is really happening inside#
- An array’s superpower is arithmetic. The address of item
i is base + i * item_size. One multiply, one add, done. That is why access is constant time regardless of size.
- A dynamic array grows by a factor, not by a fixed amount. Typically it doubles, or grows by 1.5 times. Growing by one each time would make appending n items cost n-squared work in total.
- Because it doubles, appending n items costs about 2n copies overall, so each append averages constant time even though some individual appends are expensive.
- A linked list’s superpower is that inserting needs no shifting. Its weakness is that every item costs an extra pointer of memory and lives at an unpredictable address, which defeats the CPU cache.
- A stack and a queue are usually not separate structures at all. They are restricted interfaces on top of an array or a linked list.
- A binary search tree can degenerate. Insert 1, 2, 3, 4, 5 in order into a plain tree and you get a straight line: a linked list with extra steps, and lookups become linear.
- A balanced tree fixes this by rotating itself after inserts to keep the height near the logarithm of the item count. AVL trees (1962) and red-black trees (1978) are the two you meet.
- A heap is stored as a plain array with an implicit shape rule: the children of index
i sit at 2i+1 and 2i+2. No pointers at all.
- A trie stores one character per edge. All words sharing a prefix share a path, so lookup costs the length of the word and not the size of the dictionary.
TECHNICAL17.6.5 the engineer’s version#
- Complexities below are average case unless marked.
n is the number of elements, k the key length.
| Array |
O(1) |
O(n) |
O(n) |
| Dynamic array |
O(1) |
O(n) |
O(1) amortized end |
| Linked list |
O(n) |
O(n) |
O(1) at a known node |
| Stack, queue, deque |
O(1) ends |
O(n) |
O(1) |
| Hash table |
n/a |
O(1) avg, O(n) worst |
O(1) avg |
| Balanced BST |
O(log n) |
O(log n) |
O(log n) |
| Binary heap |
O(1) min |
O(n) |
O(log n) |
| Trie |
n/a |
O(k) |
O(k) |
| Array |
O(n) |
no |
none |
| Dynamic array |
O(n) |
no |
up to 100 percent |
| Linked list |
O(1) at node |
no |
1 pointer each |
| Hash table |
O(1) avg |
no |
buckets plus slack |
| Balanced BST |
O(log n) |
yes |
2 pointers each |
| Binary heap |
O(log n) |
partial |
none |
| Trie |
O(k) |
yes, by prefix |
high |
- Real implementations. C++
std::vector is a dynamic array, std::map is a red-black tree, std::unordered_map is a hash table with chaining. Java ArrayList is a dynamic array, HashMap uses chaining and converts a bucket to a red-black tree once it holds 8 or more entries, a change made in Java 8 in 2014 to blunt hash-collision denial-of-service attacks.
- Python
dict has been insertion-ordered since CPython 3.6 (an implementation detail then) and by language guarantee since 3.7 in 2018. It uses open addressing with a compact index array.
- Go maps are hash tables with 8-slot buckets and randomized iteration order, deliberately, so that nobody writes code depending on the order.
- Hash functions in production: SipHash-1-3 in Python and Rust’s default hasher, chosen for resistance to hash-flooding attacks rather than raw speed; FNV-1a and xxHash where speed matters more; wyhash in several newer maps.
- Python’s
hash() for strings is randomized per process by default since Python 3.3 in 2012, after the 2011 hash-collision denial-of-service disclosure. Set PYTHONHASHSEED to make it repeatable.
- Cache behaviour dominates the constant factors. A cache line is 64 bytes on x86-64 and on Apple silicon. An array of 4-byte integers gets 16 per line for free. A linked list of nodes scattered across the heap gets one useful item per cache miss, and a main-memory miss costs on the order of 60 to 100 nanoseconds while an L1 hit costs about 1 nanosecond.
- Graph representations: adjacency list uses O(V + E) space and is right for sparse graphs; adjacency matrix uses O(V^2) and gives O(1) edge lookup, right only for dense graphs.
- Tools:
sys.getsizeof in Python, jol for Java object layout, perf stat -e cache-misses on Linux, and heaptrack for allocation profiles.
WORDS17.6.6 remember these#
- Array — numbered slots side by side — contiguous storage with O(1) indexed access by address arithmetic.
- Dynamic array — an array that grows — geometric reallocation giving amortized O(1) append.
- Linked list — a chain of items — nodes with pointers, O(1) splice, poor cache locality.
- Hash table — compute the drawer, open it — key-to-bucket mapping with collision resolution by chaining or open addressing.
- Collision — two keys, one bucket — distinct keys hashing to the same index, unavoidable by the pigeonhole principle.
- Load factor — how full it is — entries divided by buckets, the trigger for resize, typically 0.7 to 0.9.
- Balanced tree — a tree that stays short — a BST with rebalancing keeping height O(log n), such as AVL or red-black.
- Amortized — expensive rarely, cheap usually — the average cost per operation over a worst-case sequence, not a probabilistic average.
17.7 Algorithms and Big-O#
PLAIN17.7.1 in simple words#
- An algorithm is a finite recipe for turning an input into an answer.
- The same job usually has many algorithms, and they differ enormously in how the work grows as the input grows.
- Big-O notation is a short way of saying how the work grows.
- It ignores constants and small terms on purpose. It answers “what happens when the input gets ten times bigger”, not “how many milliseconds”.
- O(1) means the work does not depend on the size at all.
- O(n) means ten times the data takes ten times as long.
- O(n^2) means ten times the data takes a hundred times as long.
- O(log n) means doubling the data adds one step. This is the good one.
- O(2^n) means adding one item doubles the time. This is the one that kills you.
- Big-O is about scale, not speed. For 10 items, anything works. For 10 million, the choice decides whether the program finishes today.
PLAIN17.7.2 a picture in your head#
- You are looking for a name in a phone book of one million entries.
- Reading every page from the front is linear search, O(n). Worst case, a million looks.
- Opening the middle, deciding which half, and repeating is binary search, O(log n). Twenty looks, because 2^20 is just over a million.
- Twenty against a million. That is the whole reason algorithms matter.
- Now imagine comparing every entry with every other entry to find duplicates. That is O(n^2), which is a million million comparisons. At a billion comparisons a second, about 11 days.
- Sorting first and then scanning once is O(n log n), which is about 20 million steps, or a fiftieth of a second.
Where this comparison breaks: binary search only works because the phone book is already sorted. Big-O hides the cost of getting there. It also hides constants, and constants matter in the real world: an O(n log n) algorithm with a huge constant can lose to an O(n^2) one for every input you will ever actually see. This is exactly why library sorts switch to insertion sort for small chunks.
PLAIN17.7.3 a worked example#
- Assume a rough one hundred million simple operations per second, which is a reasonable figure for interpreted code and pessimistic for compiled code. These figures are approximate and meant for ranking, not for prediction.
| O(1) |
constant |
any size |
| O(log n) |
logarithmic |
effectively unlimited |
| O(n) |
linear |
100,000,000 |
| O(n log n) |
linearithmic |
about 4,000,000 |
| O(n^2) |
quadratic |
about 10,000 |
| O(n^3) |
cubic |
about 460 |
| O(2^n) |
exponential |
about 26 |
| O(n!) |
factorial |
about 11 |
- Read the bottom two rows again. An exponential algorithm handles 26 items in a second. Twenty-six. Not 26 thousand.
- Now dynamic programming, with the classic example. Fibonacci numbers, where each is the sum of the two before it.
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
- This is correct and it is O(2^n).
fib(40) makes about 331 million calls, because fib(38) is computed twice, fib(37) three times, and so on.
- The insight of dynamic programming: the sub-problems repeat, so solve each one once and write the answer down.
def fib(n, seen={}):
if n < 2:
return n
if n not in seen:
seen[n] = fib(n - 1, seen) + fib(n - 2, seen)
return seen[n]
- That change makes it O(n).
fib(40) now takes 40 additions instead of 331 million calls. The algorithm did not get cleverer. It stopped repeating itself.
- Writing answers down as you go is called memoization, and it is the top-down form. Filling a table from the smallest case upward is the bottom-up form. They compute the same thing.
PLAIN17.7.4 what is really happening inside#
- Recursion means a function calls itself. Each call gets its own stack frame: a slice of the stack holding its arguments, its local variables and the address to return to.
main -> frame 1
fib(4) -> frame 2
fib(3) -> frame 3
fib(2) -> frame 4
fib(1) -> frame 5 returns 1, frame popped
fib(0) -> frame 5 returns 0, frame popped
- Frames stack up as you go deeper and are popped as each call returns. If you never stop going deeper, the stack runs out. That is a stack overflow.
- Default limits: CPython refuses at about 1,000 nested calls by default; the main thread stack on Linux is typically 8 MB, which is thousands of frames for compiled code.
- Now sorting, in order of how they work.
- Bubble sort: repeatedly walk the list swapping neighbours that are out of order. O(n^2). Never use it, but understand it.
- Insertion sort: take each item and slide it back into its place among the already-sorted front. O(n^2) in general, but O(n) if the data is nearly sorted, and very fast for small lists.
- Merge sort: split in half, sort each half, then merge the two sorted halves in one pass. Always O(n log n). Needs extra space. Stable, meaning equal items keep their original order.
- Quicksort: pick a pivot, move everything smaller left and larger right, then sort each side. O(n log n) on average, O(n^2) if you keep picking bad pivots. Sorts in place, and is usually the fastest in practice.
- Greedy against exhaustive. A greedy algorithm takes the best-looking step at each moment and never reconsiders. It is fast and sometimes optimal.
- Giving change with British coins greedily always gives the fewest coins. With a hypothetical set of 1, 3 and 4, greedy makes 6 as 4+1+1, three coins, while the best answer is 3+3, two coins. Greedy is a gamble that needs proof.
- Exhaustive search tries every possibility. It always finds the best answer and it is usually exponential.
TECHNICAL17.7.5 the engineer’s version#
- Formally, f(n) is O(g(n)) if there exist positive constants c and n0 such that f(n) <= c*g(n) for all n >= n0. Big-O is an upper bound. Big-Omega is a lower bound, and Big-Theta is both.
- Comparison sorting has a proven lower bound of Omega(n log n) comparisons, because a decision tree with n! leaves has height at least log2(n!). Counting sort and radix sort beat it by not comparing.
- What real libraries actually ship:
Python sorted |
Timsort |
2002, Tim Peters |
| Java objects |
Timsort |
Java 7, 2011 |
| Java primitives |
dual-pivot quicksort |
Java 7, 2011 |
C++ std::sort |
introsort |
Musser, 1997 |
Go slices.Sort |
pattern-defeating quicksort |
Go 1.19, 2022 |
Rust sort_unstable |
quicksort variant |
in-place, no alloc |
- Timsort was written by Tim Peters for CPython in 2002. It finds existing sorted runs, extends short ones with binary insertion sort, and merges them under strict invariants. It is stable and it is O(n) on already-sorted data. Java adopted it for object arrays in Java 7 in 2011.
- Introsort, described by David Musser in 1997, starts as quicksort, switches to heapsort if recursion gets too deep, and finishes with insertion sort on small partitions. It gets quicksort’s speed with heapsort’s O(n log n) worst-case guarantee.
- Dual-pivot quicksort was contributed by Vladimir Yaroslavskiy in 2009 and is used for Java primitive arrays, where stability is meaningless because equal integers are indistinguishable.
- Dynamic programming was named by Richard Bellman around 1953 at RAND. He chose the words partly because they sounded impressive to a defence secretary who disliked mathematical research. It requires two properties: optimal substructure and overlapping subproblems.
- Standard dynamic programming problems and their complexities: 0/1 knapsack O(nW), longest common subsequence O(nm), edit distance O(nm), Floyd-Warshall all-pairs shortest paths O(V^3).
- Greedy algorithms that are provably optimal: Dijkstra’s shortest path (1959, Edsger Dijkstra), Kruskal’s and Prim’s minimum spanning trees, and Huffman coding (David Huffman, 1952). Each has a proof, usually via a matroid or an exchange argument.
- Tools:
timeit in Python, JMH for Java, perf record and perf report on Linux, cargo bench and pprof for Go.
WORDS17.7.6 remember these#
- Algorithm — a finite recipe that terminates — a well-defined procedure mapping input to output in finite steps.
- Big-O — how the work grows with size — an asymptotic upper bound on a cost function, ignoring constants and lower-order terms.
- Binary search — halve the range each time — O(log n) search on a sorted sequence with random access.
- Stable sort — equal items keep their order — a sort preserving the relative order of records with equal keys.
- Stack frame — one call’s private space — the activation record holding arguments, locals and the return address.
- Memoization — write the answer down — caching results of pure function calls keyed by arguments.
- Greedy — best-looking step, never reconsider — a locally optimal choice strategy, globally optimal only where provable.
17.8 Concurrency and parallelism#
PLAIN17.8.1 in simple words#
- Concurrency is dealing with many things at once. Parallelism is doing many things at once.
- One cook juggling three pans is concurrent. Three cooks each with one pan is parallel.
- A single-core machine can be concurrent and cannot be parallel. Concurrency is about structure. Parallelism is about hardware.
- A process is a running program with its own private memory. Two processes cannot touch each other’s data by accident.
- A thread is a strand of execution inside a process. Threads in one process share all memory, which is fast and dangerous.
- When two threads touch the same data at the same time and the result depends on who got there first, that is a race condition.
- A lock is a token. Only the thread holding it may touch the protected data. Everyone else waits.
- If two threads each hold a lock the other needs, both wait forever. That is deadlock.
- Concurrency is genuinely hard because the bugs depend on timing, appear once in ten thousand runs, and vanish when you add a print statement.
PLAIN17.8.2 a picture in your head#
- Picture a shared kitchen notebook holding a running total.
- Two people each have twenty receipts to add.
- Adding one receipt takes three actions: read the total, work out the new total in your head, write it back.
- Person A reads 41. Before A writes, person B also reads 41. A works out 42 and writes it. B works out 42 and writes it.
- Two receipts were added. The total went up by one. Nothing was broken, nobody made a mistake, and the answer is wrong.
- A lock is a rule: pick up the pen before reading, put it down after writing. Only one person can hold the pen.
- Deadlock: A holds the pen and needs the calculator; B holds the calculator and needs the pen. Neither will let go. The kitchen stops.
Where this comparison breaks: people notice when they are stuck. Threads do not. Also, real processors and compilers may reorder your reads and writes for speed, so the order you wrote is not always the order that happens. That is a whole extra layer of difficulty the notebook has no equivalent for.
PLAIN17.8.3 a worked example#
- Here is the notebook, in Python, with two real threads.
import threading
counter = 0
def bump():
global counter
for _ in range(200000):
counter += 1
t1 = threading.Thread(target=bump)
t2 = threading.Thread(target=bump)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter) # expected 400000, often less
counter += 1 looks like one action. It is three: load, add, store.
- Here is one bad interleaving, step by step.
step thread A thread B counter
1 load 41 41
2 load 41 41
3 add -> 42 41
4 add -> 42 41
5 store 42 42
6 store 42 42
- Two increments happened. The counter rose by one. This is a lost update.
- The fix is a lock around the three steps.
lock = threading.Lock()
def bump():
global counter
for _ in range(200000):
with lock:
counter += 1
- Now the three steps are one indivisible unit. The section of code the lock protects is called the critical section.
- The cost is real: this version is several times slower, because threads spend time waiting instead of working.
PLAIN17.8.4 what is really happening inside#
- A mutex is a lock allowing exactly one holder. The name is short for mutual exclusion. Acquiring an uncontended mutex is cheap, tens of nanoseconds. Acquiring a contended one means the operating system puts your thread to sleep and wakes it later, costing microseconds.
- An atomic operation is one the hardware guarantees cannot be split.
lock xadd on x86 does load-add-store as a single unit. For a simple counter this is faster than a lock and needs no waiting.
- Deadlock needs four conditions at once, described by Edward Coffman in 1971: mutual exclusion, hold and wait, no preemption, and circular wait. Break any one and deadlock becomes impossible.
- The usual practical fix is lock ordering: every thread acquires locks in the same global order, which breaks circular wait.
- Async and await are a different approach entirely. Instead of many threads, you have one thread and an event loop.
- The loop holds a queue of ready tasks. It runs one until that task hits something slow, like waiting for the network. The task says “I am waiting” and hands control back. The loop runs another task.
+----------------+
| ready queue |
+-------+--------+
|
v
+------------------------+
| run task until await |
+-----------+------------+
|
waiting? | yes -> park it, register callback
| no -> task finished, drop it
v
back to ready queue when the wait completes
- So one thread can juggle ten thousand network connections, because almost all of them are waiting almost all of the time.
- This is concurrency without parallelism, and without locks, because only one thing runs at a time. The trade is that any task doing real computation blocks everything.
- A coroutine is a function that can pause in the middle and resume later, keeping its local variables. That is the machinery
await is built on.
- Message passing avoids shared memory entirely. Two workers never touch the same data. They send copies to each other through a channel. Nothing is shared, so nothing needs locking.
- The actor model takes that further: an actor is an isolated unit with private state and a mailbox. It only reacts to messages. Erlang and Elixir are built entirely this way.
TECHNICAL17.8.5 the engineer’s version#
- Processes versus threads on Linux: both are tasks to the kernel, created by
clone with different sharing flags. A process context switch costs more because it changes the page tables and flushes TLB entries. Rough figures on modern x86-64: thread switch about 1 to 2 microseconds, process switch somewhat more.
- Memory ordering is a specification, not an implementation detail. The Java Memory Model was rewritten in JSR-133 for Java 5 in 2004. The C++11 memory model, adopted in 2011, defines
memory_order_relaxed, acquire, release and seq_cst. x86-64 gives total store order almost for free; ARM and RISC-V are weakly ordered and need explicit barriers.
- Amdahl’s law, stated by Gene Amdahl in 1967: if a fraction p of a program is parallelizable, maximum speedup on N cores is 1 / ((1 - p) + p/N). With p = 0.95, the ceiling is 20 times no matter how many cores you buy.
- Concurrency models and their origins:
| Threads and locks |
1960s, POSIX 1995 |
C, C++, Java |
| CSP channels |
Hoare, 1978 |
Go, occam |
| Actors |
Hewitt, 1973 |
Erlang, Akka |
| Event loop |
Unix select, 1983 |
Node.js, asyncio |
| Ownership + Send/Sync |
Rust, 2015 |
Rust |
- Go’s goroutines are user-space threads multiplexed onto operating-system threads by the runtime, starting at an 8 KB stack that grows as needed, which is why a million goroutines is routine and a million OS threads is not.
- Erlang, created by Joe Armstrong and colleagues at Ericsson from 1986, runs millions of isolated processes with no shared memory and a “let it crash” supervision model. The AXD301 switch built on it is the source of the often-quoted nine nines availability figure, which is a marketing claim about one deployment and not a property of the language.
- The Global Interpreter Lock in CPython is a single mutex that ensures only one thread executes Python bytecode at a time. It makes reference counting safe and single-threaded code fast. It means CPU-bound Python threads do not scale across cores. Input/output releases it, so I/O-bound threading works.
- This is changing, with dates. PEP 703 was accepted in July 2023. Python 3.13, released October 2024, shipped an optional experimental free-threaded build. Python 3.14, released 7 October 2025, moved that build to officially supported, though it is still not the default. Established fact: the option exists. Active question: what the single-threaded performance cost settles at, and how quickly C extensions adapt.
- Rust prevents data races at compile time using the
Send and Sync marker traits plus the borrow checker. It does not prevent deadlock, which is a liveness property, and the documentation says so plainly.
- Tools: ThreadSanitizer (
-fsanitize=thread), Helgrind under Valgrind, perf sched, Go’s -race flag, and jstack for JVM thread dumps.
WORDS17.8.6 remember these#
- Concurrency — dealing with many things at once — composing independently executing tasks, whether or not they overlap in time.
- Parallelism — doing many things at once — simultaneous execution on multiple hardware execution units.
- Race condition — the answer depends on timing — unsynchronized access where at least one access is a write.
- Mutex — a token only one may hold — a mutual exclusion primitive serializing entry to a critical section.
- Deadlock — everyone waiting forever — a cycle of threads each holding a resource another needs, requiring all four Coffman conditions.
- Atomic — cannot be split in half — an operation the hardware performs indivisibly, such as compare-and-swap.
- Event loop — one worker, many waits — a dispatcher polling readiness and running callbacks or resuming coroutines on a single thread.
- GIL — one Python thread at a time — CPython’s global interpreter lock, optional to disable since Python 3.13 and supported since 3.14.
17.9 Errors and correctness#
PLAIN17.9.1 in simple words#
- Things go wrong. Files are missing, networks drop, users type nonsense.
- There are two main ways a function tells you it failed.
- Return codes: the function returns a value meaning “it did not work”, and the caller must check it. Simple, explicit, and easy to ignore.
- Exceptions: the function throws, and control jumps out to whoever declared they would handle it. Hard to ignore, but the jump is invisible when reading the code.
- A panic is a third thing: the program has found a state that should be impossible and stops rather than continuing wrongly.
- The rule of thumb: expected problems are errors, and you handle them. Impossible states are bugs, and you crash on them.
- An assertion is a statement of something you believe must be true. If it is false, stop immediately.
- A test is code that runs your code and checks the answer, automatically, every time.
PLAIN17.9.2 a picture in your head#
- Think about a parcel delivery.
- A return code is the driver leaving a card saying “not delivered, reason 3”. You have to read the card. Nothing forces you to.
- An exception is the driver pulling a fire alarm that keeps sounding until someone on some floor takes responsibility. If nobody does, the whole building is evacuated, which is the program crashing.
- A panic is the driver finding the building on fire and refusing to enter.
- An assertion is a sign on the loading bay saying “this door must be unlocked during working hours”. Nobody expects it to be locked. If it is, something deeper is wrong and continuing makes it worse.
Where this comparison breaks: fire alarms are rare and exceptions are not. In many programs, a file not existing is completely normal and gets thrown dozens of times a second, which is why some languages treat exceptions as a bad fit for ordinary control flow and use return values instead.
PLAIN17.9.3 a worked example#
- Four languages, the same job: open a file that may not be there.
FILE *f = fopen("data.txt", "r");
if (f == NULL) {
perror("cannot open");
return 1;
}
try:
f = open("data.txt")
except FileNotFoundError as e:
print("cannot open:", e)
f, err := os.Open("data.txt")
if err != nil {
return fmt.Errorf("cannot open: %w", err)
}
let f = match File::open("data.txt") {
Ok(f) => f,
Err(e) => return Err(e.into()),
};
- C returns a null pointer. Nothing stops you using it, and the crash comes later somewhere else.
- Python throws. If you write no
try, the program stops with a stack trace pointing at the exact line.
- Go returns two values and the second is the error. You can ignore it, but the compiler complains about the unused variable and every linter shouts.
- Rust returns a
Result, which is either Ok or Err, and you cannot get the file out without dealing with both. Rust also has the ? operator, which is shorthand for exactly the match above.
- Now a real test, in Python’s built-in style.
def slugify(text):
return text.strip().lower().replace(" ", "-")
def test_slugify_basic():
assert slugify("Hello World") == "hello-world"
def test_slugify_trims():
assert slugify(" Spaced Out ") == "spaced-out"
- Run it with
pytest. Two tests, two green ticks, in about 10 milliseconds.
- Now a property-based test, which invents inputs instead of using yours.
from hypothesis import given, strategies as st
@given(st.text())
def test_slug_has_no_whitespace(s):
assert not any(c.isspace() for c in slugify(s))
- This one fails, quickly, with the input
"\t". A tab is whitespace, it is not a space, and replace(" ", "-") never touches it.
- That is the value of property-based testing in one example. It found a real bug from a rule, not from a guess about which inputs to try.
PLAIN17.9.4 what is really happening inside#
- Throwing an exception is not a normal return. The runtime walks back up the stack, frame by frame, looking for a handler that matches, running cleanup code for each frame it discards. This is called stack unwinding.
- That is why exceptions are cheap when not thrown and expensive when thrown. Modern implementations use zero-cost tables, so the non-throwing path has no overhead at all and the throwing path costs microseconds.
- Assertions are usually compiled out of release builds. In C,
NDEBUG removes them. This means an assertion must never contain something the program needs to do, because in production it will not happen.
- Defensive programming means checking inputs at the boundary of your code, where data arrives from outside, and trusting them inside.
- The mistake is checking everywhere. Every internal function re-validating makes code longer, slower, and harder to read, without making it safer.
- Testing comes in layers.
- A unit test tests one function in isolation, in milliseconds, with no database and no network.
- An integration test tests several parts together, usually with a real database, in seconds.
- An end-to-end test drives the whole system the way a user would, in tens of seconds to minutes, and is the most fragile.
- A property-based test states a rule that should always hold and lets the tool generate hundreds of inputs trying to break it.
- The rough guidance is a pyramid: many unit tests, fewer integration tests, very few end-to-end tests. The reason is cost and flakiness, not purity.
TECHNICAL17.9.5 the engineer’s version#
- Error strategies by language, stated exactly:
| C |
int or null return, errno |
yes, silently |
| Java |
checked and unchecked |
checked, no |
| Python |
exceptions only |
yes, uncaught crashes |
| Go |
explicit error value |
yes, linters object |
| Rust |
Result<T, E> |
no, #[must_use] |
- Java’s checked exceptions, present since Java 1.0 in 1996, force the caller to declare or handle them. Experts genuinely disagree about this: supporters say it makes failure part of the type signature; critics, including the C# design team who deliberately left them out, say it produces empty catch blocks and
throws Exception on every method.
- Rust distinguishes recoverable errors (
Result) from unrecoverable ones (panic!). A panic unwinds by default and can be configured to abort with panic = "abort" in Cargo, which produces smaller binaries.
- Go added error wrapping with
%w and the errors.Is and errors.As functions in Go 1.13, released September 2019.
- Exception implementation: Itanium C++ ABI zero-cost exceptions use side tables consulted only during unwinding, so entering a
try block costs nothing. Setjmp/longjmp implementations cost on entry instead and are largely historical.
- Testing frameworks and origins: JUnit, Kent Beck and Erich Gamma, 1997, which defined the xUnit shape copied by almost everything since. QuickCheck, the first property-based tester, by Koen Claessen and John Hughes for Haskell in
- Hypothesis brought it to Python from 2013.
- Coverage is a diagnostic, not a target. 100 percent line coverage proves every line ran, not that any behaviour was checked. Branch coverage is stricter and still not proof. Tools:
coverage.py, JaCoCo, go test -cover, cargo llvm-cov.
- Formal methods sit beyond testing: TLA+, created by Leslie Lamport in the 1990s, is used by AWS to check distributed protocol designs, and the seL4 microkernel has a machine -checked functional correctness proof, completed in 2009. These are real and they are expensive, so they are used where failure is catastrophic.
- Tools:
pytest -x --lf, go test -race ./..., cargo test, mvn verify, and mutation testers such as mutmut and PIT, which change your code deliberately to see whether any test notices.
WORDS17.9.6 remember these#
- Return code — a value meaning it failed — an out-of-band or sentinel result the caller must inspect.
- Exception — a jump to whoever handles it — a non-local transfer of control with stack unwinding and frame cleanup.
- Panic — stop, this should be impossible — an unrecoverable fault that aborts or unwinds rather than returning.
- Assertion — a claim that must be true — a runtime check of an invariant, typically compiled out in release builds.
- Unit test — check one piece alone — an isolated, fast, deterministic test of a single unit of behaviour.
- Property-based test — state a rule, let it hunt — automated generation of inputs against an invariant, with shrinking to a minimal failing case.
- Coverage — how much code the tests ran — the proportion of lines or branches executed, a diagnostic rather than a goal.
17.10 A tour of the major languages#
PLAIN17.10.1 in simple words#
- There are about twenty languages you will actually meet, and they sort into a few families.
- Systems languages compile straight to machine code and give you control of memory: C, C++, Rust, and Go in a gentler way.
- Managed languages run on a virtual machine with automatic memory: Java, C#, Kotlin.
- Scripting languages run from source with no separate build step: Python, JavaScript, Ruby, PHP.
- Platform languages exist mainly to build for one place: Swift for Apple, Kotlin for Android.
- Specialist languages do one job extremely well and are bad at everything else: SQL for data, Bash for gluing commands, R and MATLAB for numbers.
- Underneath all of them is assembly, one line per machine instruction.
- For each language below: the year, the person, what it was made for, what it is genuinely good at, its real weakness, and where you meet it in 2026.
PLAIN17.10.2 a picture in your head#
- Think of vehicles rather than a ladder of quality.
- C is a motorcycle: light, fast, nothing between you and the road, and no protection when you fall.
- Java is a coach: heavy, slow to start, carries a hundred passengers reliably for twenty years.
- Python is a bicycle: anyone can ride it today, it goes almost anywhere, and it will not win a race.
- Rust is a motorcycle with a mandatory riding test that you must pass before the engine will start.
- SQL is a train: it goes only where the rails go, and on those rails nothing beats it.
- Nobody asks which vehicle is best. They ask what the journey is.
Where this comparison breaks: vehicles cannot be combined, and languages are combined constantly. A single 2026 web product routinely uses TypeScript in the browser, Go on the server, SQL in the database, Python for the model, and C underneath all four.
PLAIN17.10.3 a worked example#
- The systems languages, with a sample each.
- C. 1972, Dennis Ritchie at Bell Labs. Made to rewrite the Unix operating system in something portable. Genuinely good at: direct hardware access, a near-zero runtime, and being the language every other language talks to. Main weakness: no memory safety of any kind. In 2026 you meet it in the Linux kernel, embedded firmware, and inside the runtime of nearly every language in this list.
for (int i = 0; i < n; i++) sum += a[i];
- C++. Begun 1979 as “C with Classes” by Bjarne Stroustrup at Bell Labs, released 1985. Made to add Simula’s classes to C for large simulations. Good at: zero-overhead abstraction, the fastest code you can still structure. Weakness: enormous, with forty years of features that never leave. In 2026: game engines such as Unreal, browsers, databases, and trading systems. The C++26 standard replaced C++23 in March 2026.
std::sort(v.begin(), v.end());
auto n = std::count_if(v.begin(), v.end(), is_big);
- Go. Announced November 2009 by Robert Griesemer, Rob Pike and Ken Thompson at Google. Made for large server software with fast builds. Good at: simplicity you can hold in your head, built-in concurrency, one static binary with no dependencies, and compiles measured in seconds. Weakness: deliberately plain, and error handling is repetitive on purpose. In 2026: Docker, Kubernetes, and most cloud infrastructure. Go 1.26 landed 10 February
go worker(ch) // start a goroutine
msg := <-ch // receive from a channel
- Rust. Started 2006 as Graydon Hoare’s personal project, sponsored by Mozilla from 2009, version 1.0 on 15 May 2015. Made for systems work without memory bugs. Good at: memory safety with no garbage collector, and concurrency the compiler checks. Weakness: a steep learning curve and slow compiles. In 2026: Firefox, Cloudflare, Android system components, and Linux kernel drivers, since Rust support was merged into Linux 6.1 in December
- Stable release in August 2026 is 1.97.
let names: Vec<String> =
people.iter().map(|p| p.name.clone()).collect();
PLAIN17.10.4 what is really happening inside#
- The managed and scripting languages.
- Java. 1995, James Gosling at Sun Microsystems, from a 1991 project called Oak. Made so one compiled program could run on any machine with a virtual machine. Good at: very large long-lived server systems, superb tooling and profilers, mature garbage collectors. Weakness: verbose, slow to start, heavy on memory. In 2026: banking, enterprise back-ends, and Android underneath. Java 25 is the current long-term-support release, from 16 September 2025.
List<String> big = names.stream()
.filter(n -> n.length() > 3).toList();
- C#. Announced 2000, released 2002 with .NET 1.0, designed by Anders Hejlsberg at Microsoft. Made as Microsoft’s answer to Java. Good at: a clean modern design, LINQ for querying collections, and it shipped
async/await in C# 5 in 2012, which most other languages then copied. Weakness: was tied to Windows until .NET Core in 2016. In 2026: Unity games, Windows desktop, and enterprise services on .NET 10, released November 2025.
var big = names.Where(n => n.Length > 3).ToList();
- Kotlin. Announced July 2011 by JetBrains, version 1.0 in February 2016. Made as a less verbose Java that runs on the same virtual machine. Good at: null safety in the type system, coroutines, and complete Java interop. Weakness: still bound to the JVM tooling world, and compiles more slowly than Java. In 2026: Google named it the preferred Android language in 2019, and most new Android code is Kotlin.
val name: String? = user.name
println(name?.length ?: 0)
- Swift. Announced June 2014 at Apple’s developer conference, led by Chris Lattner. Made to replace Objective-C. Good at: safe and modern while compiling to fast native code, with automatic reference counting instead of a collector. Weakness: in practice you meet it almost only on Apple platforms. In 2026: essentially all new iOS and macOS applications.
let big = names.filter { $0.count > 3 }
- Python. First release February 1991, by Guido van Rossum at CWI in the Netherlands. Made as a readable scripting language for people who found the alternatives painful. Good at: readability, gluing other systems together, and an unmatched scientific and machine-learning ecosystem. Weakness: slow, and packaging has been a long-running sore point. In 2026: machine learning, data work, automation, and back-ends. Version 3.14 arrived 7 October 2025.
big = [n for n in names if len(n) > 3]
- JavaScript. 1995, Brendan Eich at Netscape, with the first version written in about ten days in May 1995. Made to add small interactions to web pages. Good at: it runs everywhere, in every browser on earth, with the largest package ecosystem in existence. Weakness: weak typing and a set of historical oddities that can never be removed. In 2026: every browser, plus servers through Node.js, Deno and Bun.
- TypeScript. Released 1 October 2012, designed by Anders Hejlsberg and Luke Hoban at Microsoft. It is JavaScript plus a static type system, erased before running. Good at: catching whole classes of mistakes before the code ships. Weakness: another build step, and the types are checked but not enforced at runtime. In 2026 it is the default for serious front-end work. TypeScript 6.0 shipped 23 March 2026, and version 7.0, a rewrite of the compiler in Go for roughly a tenfold speedup, is in preview.
const big: string[] = names.filter(n => n.length > 3);
- PHP. 1995, Rasmus Lerdorf, originally a set of tools he called Personal Home Page. Made to put dynamic content into web pages. Good at: the simplest deployment story there is, and universal cheap hosting. Weakness: an inconsistent standard library and a poor security reputation earned in its early years. In 2026: WordPress, which runs a large fraction of all websites, plus Wikipedia and the Laravel framework. PHP 8.5 arrived 20 November 2025.
- Ruby. 1995, Yukihiro Matsumoto in Japan, designed explicitly for programmer happiness. Good at: expressive code and building small domain-specific languages, which is why Rails felt like magic in 2004. Weakness: slow, and the ecosystem shrank after about 2015. In 2026: GitHub, Shopify, and a large body of Rails applications.
big = names.select { |n| n.length > 3 }
TECHNICAL17.10.5 the engineer’s version#
- The specialist languages.
- SQL. Designed 1974 as SEQUEL by Donald Chamberlin and Raymond Boyce at IBM San Jose, implementing Edgar Codd’s 1970 relational model. Declarative: you state the result, the query planner chooses the method. Good at: set operations over large relational data with an optimizer you did not write. Weakness: dialects differ enough that portable SQL is a discipline, and it is not a general-purpose language. Current standard is SQL:2023. Everywhere data is stored in 2026.
SELECT country, COUNT(*) AS n
FROM users WHERE active GROUP BY country
HAVING COUNT(*) > 100 ORDER BY n DESC;
- Bash. 1989, Brian Fox for the GNU project, as a free replacement for the 1979 Bourne shell. Good at: joining programs with pipes and automating a machine in a few lines. Weakness: quoting rules are genuinely treacherous and error handling is weak, which is why
set -euo pipefail is standard advice. In 2026: continuous-integration pipelines, container images, and every server login.
set -euo pipefail
grep -c ERROR /var/log/app.log || echo 0
- R. 1993, Ross Ihaka and Robert Gentleman at the University of Auckland, as a free implementation of the S language John Chambers created at Bell Labs in 1976. Good at: statistics, and plotting through ggplot2. Weakness: unusual evaluation semantics and poor performance outside vectorized work. In 2026: academic statistics, biostatistics and epidemiology.
- MATLAB. Written by Cleve Moler in the late 1970s at the University of New Mexico as a friendly front end to the LINPACK and EISPACK Fortran libraries, and sold commercially from 1984 when MathWorks was founded. Good at: matrix and numerical work, and Simulink for control system modelling. Weakness: a paid commercial licence, and 1-based indexing that surprises everyone else. In 2026: control engineering, signal processing, and university courses.
- Assembly. Not one language but one per instruction set. Kathleen Booth is credited with writing the first assembly language, working at Birkbeck College in London from 1947. One mnemonic maps to one machine instruction. Good at: exact control and hand-tuning the few functions that matter. Weakness: not portable at all, and enormous effort per line. In 2026: boot code, cryptographic primitives, and compiler output you read while debugging.
mov eax, [rdi] ; load a 32-bit value
add eax, 1 ; add one
mov [rdi], eax ; store it back
- Verified summary of origins:
| Assembly |
1947 |
Kathleen Booth |
| C |
1972 |
Dennis Ritchie |
| SQL (SEQUEL) |
1974 |
Chamberlin and Boyce |
| MATLAB |
late 1970s |
Cleve Moler |
| C++ |
1979 / 1985 |
Bjarne Stroustrup |
| Bash |
1989 |
Brian Fox |
| Python |
1991 |
Guido van Rossum |
| R |
1993 |
Ihaka and Gentleman |
| Java |
1995 |
James Gosling |
| JavaScript |
1995 |
Brendan Eich |
| PHP |
1995 |
Rasmus Lerdorf |
| Ruby |
1995 |
Yukihiro Matsumoto |
| C# |
2000 / 2002 |
Anders Hejlsberg |
| Go |
2009 |
Griesemer, Pike, Thompson |
| Rust |
2010 / 1.0 in 2015 |
Graydon Hoare |
| Kotlin |
2011 / 1.0 in 2016 |
JetBrains |
| TypeScript |
2012 |
Hejlsberg and Hoban |
| Swift |
2014 |
Chris Lattner |
- Release status checked in August 2026:
| Python |
3.14 |
7 Oct 2025 |
| Java |
25 LTS, 26 current |
Sep 2025, Mar 2026 |
| Go |
1.26 |
10 Feb 2026 |
| Rust |
1.97 |
Jul 2026 |
| PHP |
8.5 |
20 Nov 2025 |
| TypeScript |
6.0, 7.0 in preview |
23 Mar 2026 |
- Popularity indexes such as TIOBE, the Stack Overflow developer survey and the GitHub Octoverse report disagree with each other every year, because they measure different things: search volume, respondent self-report, and public repository activity. Treat any single ranking as one weak signal.
WORDS17.10.6 remember these#
- Systems language — you manage the memory — compiles to native code with direct memory access and no mandatory runtime.
- Managed language — the runtime manages memory — executes on a virtual machine with automatic memory management.
- Interop — languages calling each other — a foreign function interface, most often through the C ABI.
- Static binary — one file, no dependencies — an executable with all libraries linked in, as Go produces by default.
- Erasure — types vanish before running — compile-time-only types removed before execution, as in TypeScript and Java generics.
- Dialect — the same language, different rules — vendor-specific extensions to a standard, most visible across SQL implementations.
17.11 How to choose a language for a job#
PLAIN17.11.1 in simple words#
- “Which language is best” is not a real question, because it has no fixed subject. Best at what, for whom, with what deadline.
- A better question has four parts: what must this program do, where must it run, who will maintain it, and how long must it live.
- Constraints usually decide it before taste does. An iPhone app is Swift. A browser front end is JavaScript or TypeScript. A database query is SQL.
- Where nothing forces the choice, the strongest factor is the team. A language nobody on the team knows costs six months of slower work.
- The second strongest factor is the ecosystem. The right library in an average language beats a great language with nothing written for it.
- Performance matters far less often than people believe, and far more when it matters.
- You are also allowed to pick two. Most real systems use several.
PLAIN17.11.2 a picture in your head#
- Choosing a language is like choosing a material for a building.
- Nobody argues that steel is better than wood. They ask about the span, the climate, the budget, and who is available to work it.
- A material also implies its trades. Choosing steel means hiring welders. Choosing Rust means hiring or training Rust programmers.
- And once the frame is up, changing material means rebuilding. Language choices are cheap on day one and very expensive on day 800.
Where this comparison breaks: buildings cannot be half steel and half wood in the same wall, and software routinely is. A Python service calling a Rust extension is completely normal and often the right answer.
PLAIN17.11.3 a worked example#
- Scenario one: a machine-learning model for a research paper, six weeks. Answer: Python. Not because it is fast, but because PyTorch, NumPy and pandas are there and every collaborator already reads it. The heavy numerical work happens in C++ and CUDA inside those libraries anyway.
- Scenario two: firmware for a battery-powered sensor with 64 KB of RAM. Answer: C, or Rust if the team has the appetite. No garbage collector will fit, and you need exact control over every byte and every interrupt.
- Scenario three: an internal web tool for 200 staff, needed next month. Answer: whatever the team already runs in production. Python with Django, Ruby with Rails, PHP with Laravel, or TypeScript with Node. The differences between these for this job are noise next to familiarity.
- Scenario four: a payments back-end that must run for fifteen years and be audited. Answer: Java or C#. Long-term-support releases, deep tooling, large hiring pools, and a strong culture of backwards compatibility. Java 25 is supported into the 2030s.
- Scenario five: a network proxy handling 100,000 connections per second. Answer: Go or Rust. Go if you want it working next month with cheap concurrency. Rust if the last 20 percent of performance and predictable latency without garbage collection pauses are worth the extra effort.
- Notice that in five real scenarios, “which language do I like” appeared zero times.
PLAIN17.11.4 what is really happening inside#
- The hidden costs of a language choice, in the order they bite you.
- Hiring. A language with 50,000 practitioners in your city and one with 500 are different business risks.
- Libraries. Check whether the exact thing you need exists and is maintained, before you decide, not after.
- Operations. Can your team debug it at 3am. Do profilers, tracing and crash reporting exist for it.
- Build and deploy. Go gives you one file. Python gives you a dependency tree and a virtual environment. That difference shows up every single day.
- Longevity. Ask whether the language has a paid maintainer, a standards body, or a large company depending on it. Languages with none of those have died before.
- Interop. If you can call C from it, you can reach almost anything. Nearly every language on the list can.
- Rewrites are the most expensive way to change your mind. The usual honest advice is to rewrite one component, measure, and only then decide.
TECHNICAL17.11.5 the engineer’s version#
- Rough performance ranking on CPU-bound work, taking optimized C as 1. These are order-of-magnitude figures from public benchmark suites and vary hugely by workload, so treat them as ranking only.
| C, C++, Rust |
1x |
under 1 ms |
| Go |
1x to 2x |
a few ms |
| Java, C# (warm) |
1x to 2x |
100 ms to 1 s |
| JavaScript (V8) |
2x to 10x |
tens of ms |
| Python (CPython) |
20x to 100x |
tens of ms |
- Java and C# reach near-native speed only after the just-in-time compiler has warmed up, which is why start-up and steady-state must be quoted separately, and why ahead-of-time options such as GraalVM native image exist.
- Python’s figure collapses when the work is inside NumPy or PyTorch, because the loop then runs in C or CUDA and Python only issues the calls.
- Decision inputs that are checkable rather than opinion: long-term-support policy and dates, the security advisory process, the package registry’s provenance guarantees, the presence of an official formatter and linter, and whether the specification is a published standard or a single implementation.
- Conway’s law, published by Melvin Conway in 1968, applies: your system will mirror your communication structure, so a language choice that splits the team also splits the architecture.
- Where experts genuinely disagree: whether a strong static type system pays for itself on small teams. Studies exist on both sides and none is conclusive, because the confounding variables are enormous.
WORDS17.11.6 remember these#
- Ecosystem — the libraries and tools around it — the package registry, frameworks, profilers and community that surround a language.
- Long-term support — a version kept safe for years — a release with a published multi-year window for security patches.
- Warm-up — the first minute is slower — the period before a JIT compiler has optimized the hot paths.
- Interop — calling other languages — a foreign function interface, usually via the C application binary interface.
- Rewrite risk — changing your mind costs years — the cost of porting a working system, historically underestimated by a large factor.
17.12 Reading other people’s code and writing readable code#
PLAIN17.12.1 in simple words#
- You will read far more code than you write, including your own from six months ago, which is somebody else’s code.
- So the real skill is not writing clever code. It is writing code the next person understands on the first read.
- Naming is most of it. A name should say what the thing is, not what type it is and not how it works.
d tells you nothing. days tells you the unit. days_since_signup tells you the meaning.
- A function should do one thing. If you need the word “and” to describe it, it is two functions.
- Comments should explain why, not what. The code already says what.
- A style guide is an agreement about layout so that nobody argues about it again.
- A formatter rewrites your code into that layout automatically. A linter warns about patterns known to cause bugs.
- Code review is another person reading your change before it ships.
- Technical debt is a shortcut you took on purpose, that will cost more later, like a loan.
PLAIN17.12.2 a picture in your head#
- Think of code as directions written for a stranger arriving at night.
- “Turn left at the thing” is a bad name. “Turn left at the petrol station” is a good one.
- A step that says “drive to the roundabout and also buy milk” is two steps.
- A note saying “this road is one-way northbound” is a good comment. A note saying “turn left here” next to the instruction “turn left here” is noise.
- A style guide is everyone agreeing to write distances in kilometres.
- Technical debt is the shortcut through the field. Faster tonight. Impassable after rain, and eventually you must build the road anyway, having also paid for the field.
Where this comparison breaks: directions are read once and code is read for years, by people who will change it. Code must be not only understandable but safely modifiable, which is a much higher bar.
PLAIN17.12.3 a worked example#
- Here is a real function, written badly.
def proc(d, f):
r = []
for x in d:
if x[2] > f and x[4] == 1:
r.append(x[0])
return r
- It works. Nobody can maintain it. What is
x[2], and what does 1 mean.
- The same thing, rewritten.
def active_user_ids_above(users, min_age):
"""Return ids of active users older than min_age."""
return [
user.id
for user in users
if user.age > min_age and user.is_active
]
- What changed: the function name says what comes out, the parameters are named, the fields are named instead of numbered, and the magic
1 became is_active.
- Nothing got slower. The rewrite is the same number of machine operations.
- Now comments. Here is a bad one and a good one.
i = i + 1 # add one to i
i = i + 1 # skip the header row
- The first repeats the code and will rot the moment the line changes. The second records a fact about the data that the code cannot express.
PLAIN17.12.4 what is really happening inside#
- How to read code you did not write, in order.
- Start at the entry point:
main, the route handler, the command. Do not start at the top of a file.
- Find the data structures before the functions. Once you know what the program holds, the functions usually explain themselves.
- Run it. Put a breakpoint or a print in the middle and look at real values. Five minutes of running beats an hour of reading.
- Use the tools: jump to definition, find all references, and
git log -p on a file to see how it grew and why.
- Read the tests. A good test suite is a specification with worked examples.
- Now writing for review. Keep changes small. A 50-line change gets a real review. A 2,000-line change gets a shrug and an approval, and that is a measured effect, not a joke.
- Separate a refactor from a behaviour change. Two commits, two reviews. Mixed together, nobody can see which lines actually changed the program.
- Technical debt is only debt if you intend to repay it. Write it down: a comment, a ticket, or a
TODO with a name and a date. Undocumented shortcuts are not debt, they are just a mess.
TECHNICAL17.12.5 the engineer’s version#
- Formatters and linters, with dates:
| lint |
C |
1978, Stephen Johnson |
| gofmt |
Go |
2009, no options at all |
| ESLint |
JavaScript |
2013, Nicholas Zakas |
| Prettier |
JS, CSS, more |
2017, James Long |
| Black |
Python |
2018, Lukasz Langa |
| rustfmt, clippy |
Rust |
2016 onward |
| Ruff |
Python |
2022, written in Rust |
gofmt made a design decision worth copying: one canonical format with no configuration. That ends the discussion permanently, which was the point. Black and rustfmt followed the same approach.
- Ruff is roughly one to two orders of magnitude faster than the Python-based linters it replaces, because it is written in Rust and does a single pass. That speed changes behaviour: a linter fast enough to run on every keystroke gets used.
- PEP 8, the Python style guide, was written by Guido van Rossum, Barry Warsaw and Nick Coghlan in 2001. Its 79-character line limit is a convention, not a standard, and teams commonly raise it to 88 or 100.
- Code review as a formal practice comes from Michael Fagan’s inspection method at IBM, published in 1976. Modern lightweight pull-request review is a descendant with much of the ceremony removed.
- Published review guidance, including Google’s engineering practices documents, converges on: review under 400 lines at a time, under 60 minutes at a stretch, and expect defect detection to fall sharply outside those bounds.
- “Technical debt” was coined by Ward Cunningham in a 1992 OOPSLA experience report. His original meaning was deliberately shipping a not-yet-right design to learn from it, then refactoring. The common modern usage, meaning any sloppy code, is a drift from what he wrote, and he said so publicly.
- Cyclomatic complexity, defined by Thomas McCabe in 1976, counts independent paths through a function. A value above roughly 10 is a common review threshold. It is a signal, not a rule.
- Tools:
git blame, git log -S to find when a string appeared, radon cc for Python complexity, golangci-lint, clang-tidy, and pre-commit to run all of them before a commit lands.
WORDS17.12.6 remember these#
- Refactor — change the shape, not the behaviour — a behaviour-preserving code transformation, ideally covered by existing tests.
- Linter — a tool that warns about risky patterns — static analysis flagging likely defects and style violations without running the code.
- Formatter — a tool that fixes layout — a deterministic rewriter producing a canonical source form.
- Magic number — an unexplained literal — a bare constant with no name, carrying meaning nowhere recorded.
- Code review — someone reads it before it ships — structured peer inspection of a change set prior to merge.
- Technical debt — a shortcut with interest — deliberately deferred design work, tracked and intended to be repaid.
- Cyclomatic complexity — how many ways through — the count of linearly independent paths through a function, McCabe 1976.
17.13 The standard library, packages and dependencies#
PLAIN17.13.1 in simple words#
- The standard library is the set of code that ships with the language itself. Sorting, files, dates, networking, text.
- Everything else you use is a package: code someone else wrote, published somewhere, that you download.
- A package manager is the tool that downloads packages and keeps track of which versions you have.
- Your package depends on other packages, which depend on others. Those are transitive dependencies, and there are usually far more of them than you expect.
- A version number tells you how much a new release might break. That is what semantic versioning is for.
- A lock file records the exact versions you actually used, so that the build on your machine and the build on the server are identical.
- Supply chain risk is the plain fact that installing a package means running someone else’s code, and trusting everyone they trusted.
- In March 2016, an eleven-line package was deleted and thousands of builds around the world broke within minutes.
PLAIN17.13.2 a picture in your head#
- Think of building a car from parts catalogues.
- The standard library is the parts that come in the box with the chassis.
- A package is a part you order from a supplier. It arrives quickly and works.
- But your supplier orders their bolts from someone else, who orders steel from someone else. You have contracts with one company and dependence on three hundred.
- A lock file is writing down every part number and batch, so that next year’s car is identical to this year’s.
- Supply chain risk is what happens when one bolt supplier decides to stop, or is bought by someone hostile, or ships a bad batch.
Where this comparison breaks: car parts are inspected, certified and physically present. Software dependencies are downloaded automatically by a script, often without a human ever looking, and a compromised one runs with all the privileges of your build machine.
PLAIN17.13.3 a worked example#
- Semantic versioning, from the SemVer 2.0.0 specification written by Tom Preston-Werner. A version is three numbers.
2 . 7 . 3
| | |
| | +-- PATCH: bug fixes only, safe
| +------ MINOR: new features, still compatible
+---------- MAJOR: something broke, read the notes
- So going from 2.7.3 to 2.7.4 should be safe. Going from 2.7.3 to 3.0.0 means the authors deliberately broke something.
- In an npm
package.json, "^2.7.3" means “any 2.x.y at or above 2.7.3”, and "~2.7.3" means “any 2.7.x at or above 2.7.3”.
- This is a convention, backed by a written specification, and it depends entirely on authors honouring it. They sometimes do not.
- Now the transitive problem. Install one popular web framework and count.
npm ls --all | wc -l # often over 1,000 lines
- You chose one package. You installed several hundred, from several hundred different people, and you have read none of them.
- The lock file is what makes this survivable.
package-lock.json, Cargo.lock, poetry.lock, go.sum and Gemfile.lock all record the exact resolved version of every package, and usually a cryptographic hash of its contents.
- Rule of thumb: commit the lock file for applications, so deployments are reproducible. For libraries, do not, so that whoever uses your library can resolve versions themselves.
PLAIN17.13.4 what is really happening inside#
- The left-pad incident, with dates.
- On 22 March 2016, a developer named Azer Koculu had a trademark dispute over a package named
kik. The npm registry transferred that name to the company.
- In protest he unpublished all 273 of his packages from npm.
- One of them was
left-pad: eleven lines of code that pad a string on the left with spaces.
- It was a dependency of a dependency of Babel, and through Babel of React, webpack and thousands of build systems.
- Within minutes, builds failed worldwide with a 404 error for a package almost nobody had chosen to install.
- npm restored the package from backup about two hours later, which was itself unprecedented, and then changed policy: a package cannot be unpublished after 24 hours if anything depends on it.
- The technical lesson is not “do not use small packages”. It is that your build had a single point of failure you did not know existed, controlled by a person you had never heard of.
- The security version of the same lesson is worse. If someone takes over that account, they do not break your build. They add three lines that read your environment variables and send them somewhere.
- This has happened repeatedly. In November 2018 the
event-stream package was handed to a new maintainer who added code targeting a cryptocurrency wallet. In late 2021 the accounts behind ua-parser-js, coa and rc were hijacked and used to ship malware.
- The most sophisticated known case is the xz-utils backdoor, CVE-2024-3094, found on 29 March 2024 by Andres Freund, a PostgreSQL developer who noticed that SSH logins were taking half a second longer than expected. It was the result of a patient multi-year effort to become a trusted maintainer.
TECHNICAL17.13.5 the engineer’s version#
- Package managers by language:
| JavaScript |
npm, yarn, pnpm |
npmjs.com |
| Python |
pip, uv, poetry |
PyPI |
| Rust |
Cargo |
crates.io |
| Go |
go modules |
proxy.golang.org |
| Java |
Maven, Gradle |
Maven Central |
| C# |
NuGet |
nuget.org |
| Ruby |
Bundler, gem |
RubyGems |
| PHP |
Composer |
Packagist |
- Dates: npm launched January 2010, created by Isaac Schlueter. Maven 1.0 arrived in 2004. Cargo has shipped with Rust since 1.0 in 2015 and has never had a competitor, which is a deliberate design outcome. Go modules arrived in Go 1.11 in 2018 and became the default build mode in Go 1.16 in 2021, replacing GOPATH.
- Go’s approach differs on purpose: minimal version selection picks the lowest version satisfying all requirements, rather than the highest. Combined with a checksum database, this makes builds reproducible without a traditional resolver.
go.sum and the public checksum database, plus the module proxy, mean Go verifies that a module’s contents have never changed since first seen. That is a stronger guarantee than most registries provide.
- Diamond dependency conflicts occur when A needs C version 1 and B needs C version 2. npm resolves it by installing both, nested. Maven picks one by nearest-wins. Cargo allows two major versions to coexist. Python historically cannot, which is why virtual environments exist.
- Supply chain defences that exist in 2026, with their status. Established: lock files with hashes, npm provenance attestations using Sigstore from 2023, PyPI trusted publishing, and two-factor authentication mandates on major registries. Active work: SLSA build-integrity levels and reproducible builds, which are real projects with partial adoption. Marketing claim: any product promising to “eliminate” supply chain risk.
- Log4Shell, CVE-2021-44228, disclosed 9 December 2021 with a CVSS score of 10.0, was a vulnerability in a logging library that almost no Java team had consciously chosen. It is the clearest demonstration that transitive dependencies are your responsibility whether you know their names or not.
- Standard library size is a design position. Go and Python ship large ones, Rust ships a deliberately small one and pushes the rest to crates.io, JavaScript ships almost nothing, which is exactly why
left-pad existed.
- Tools:
npm audit, pip-audit, cargo audit, govulncheck, Dependabot, syft to generate a software bill of materials, and grype to scan one.
WORDS17.13.6 remember these#
- Standard library — what comes in the box — the modules specified and shipped with the language implementation.
- Package manager — the tool that fetches code — resolves version constraints, downloads artefacts and records them.
- Transitive dependency — your dependency’s dependency — a package pulled in indirectly, usually the majority of the tree.
- Semantic versioning — major, minor, patch — SemVer 2.0.0, where a major bump signals a deliberate breaking change.
- Lock file — the exact versions you used — a pinned resolution with content hashes, giving reproducible installs.
- Supply chain risk — trusting strangers by default — the attack surface of every upstream package, maintainer account and build step.
- Software bill of materials — a parts list — a machine-readable inventory of every component in a build, in SPDX or CycloneDX format.
17.98 Common wrong ideas#
- Wrong: some languages are simply better than others. Right: every language is a set of trade-offs, and the sensible question is what the job demands in speed, safety, team skill, ecosystem and lifespan.
- Wrong: Python is a slow language. Right: a language is a written specification and has no speed. CPython is a slow implementation, and the same Python calling NumPy runs the actual work in optimized C.
- Wrong: static typing proves a program is correct. Right: it proves the shapes fit together. A program that computes the wrong average with perfectly matched types passes every type check ever written.
- Wrong: garbage collection means you cannot leak memory. Right: it means you cannot leak unreachable memory. A cache or a listener list that keeps growing is fully reachable, and will exhaust the heap exactly as a C leak would.
- Wrong:
x += 1 is a single operation, so two threads cannot corrupt it. Right: it is a load, an add and a store, and a thread can be interrupted between any two of them, which is precisely how lost updates happen.
- Wrong: Big-O tells you which code is faster. Right: it tells you how cost grows with input size. For small inputs a worse complexity with a smaller constant routinely wins, which is why library sorts fall back to insertion sort on short runs.
- Wrong: recursion is always more elegant than a loop. Right: it costs a stack frame per call, and without tail-call elimination, which most mainstream languages do not guarantee, deep recursion overflows the stack.
- Wrong: a null check everywhere is the same as an option type. Right: a null check can be forgotten and the compiler will not notice, whereas an option type cannot be unwrapped without handling the empty case.
- Wrong: more tests always mean better code. Right: coverage measures which lines ran, not which behaviours were checked. A property-based test that states a real rule can be worth a hundred example tests.
- Wrong: a tiny dependency is harmless because it is only eleven lines. Right: you are trusting an account, a registry and a maintainer, as the left-pad breakage of 22 March 2016 and the xz-utils backdoor of March 2024 both showed.
17.99 Chapter summary in 20 lines#
- A programming language is a notation with a grammar (syntax) and a meaning (semantics), defined by a document and implemented by separate tools.
- Syntax errors are caught for you; semantic errors run happily and give the wrong answer, which is where the real difficulty lives.
- Every language is built from the same parts: variables, literals, operators, expressions, statements, conditionals, loops, functions, scope and comments.
- The same ten-line program in C, Python, JavaScript, Java, Go and Rust differs only in punctuation and ceremony, not in ideas.
- A type is a set of values plus the legal operations on them; static means checked before running, dynamic means checked while running, and strong versus weak is the separate question of implicit conversion.
- Tony Hoare added the null reference to ALGOL W in 1965 and called it his billion-dollar mistake in 2009; option types are the fix because they cannot be used without handling the absent case.
- Manual memory management is fastest and admits leaks, double frees, use after free and dangling pointers; roughly 70 percent of reported vulnerabilities at Microsoft and Chromium were memory safety issues.
- Reference counting is immediate and predictable but cannot reclaim cycles; tracing collection marks from roots and sweeps the rest, and generational collectors exploit the fact that most objects die young.
- Rust’s ownership and borrowing move the whole question to compile time, giving memory safety with no runtime cost and a much stricter compiler.
- Paradigms are habits of organization: imperative, procedural, object-oriented, functional, declarative and event-driven, and real programs mix them.
- Encapsulation, inheritance and polymorphism are the object-oriented trio; deep inheritance is the honest weak point, which is why composition is preferred and why Go and Rust omit class inheritance entirely.
- Pure functions, immutability and higher-order functions with map, filter and reduce have spread into every mainstream language, whether or not the language calls itself functional.
- Data structures are trades: arrays give O(1) access and O(n) insertion, hash tables give O(1) average lookup by computing a bucket and resolving collisions, balanced trees give O(log n) with order preserved.
- Big-O describes growth, not speed; O(n log n) sorting handles millions in a second while O(2^n) handles about 26 items, and constants and cache behaviour decide the rest.
- Real libraries ship Timsort (Python 2002, Java 7 in 2011), introsort (C++, Musser 1997), dual-pivot quicksort (Java primitives, 2009) and pattern-defeating quicksort (Go 1.19, 2022).
- Dynamic programming turns exponential recomputation into linear work by solving each overlapping subproblem once and writing the answer down.
- Concurrency is structure and parallelism is hardware; a race condition is unsynchronized access where one access writes, fixed by locks, atomics, message passing, or by not sharing at all.
- Async and await are one thread and an event loop, which scales to tens of thousands of waiting connections and stalls completely on real computation; CPython’s GIL became optional in 3.13 in 2024 and supported in 3.14 in 2025.
- Errors are reported by return codes, exceptions, error values or Result types; assertions state invariants, and unit, integration, end-to-end and property-based tests each catch a different class of mistake.
- Choose a language from constraints, team and ecosystem rather than taste, write code the next reader understands, and treat every dependency as code you are choosing to run, as 22 March 2016 taught the whole industry.