Lecture · 7 slides
Types: what they can and cannot prove
The argument about static and dynamic typing is unusual in that both sides have data and the data does not settle it. Studies have been run; they find small effects in inconsistent directions. Enormous systems have been built both ways and are still running. Whatever the difference is, it is not the difference between working and not working.
That should be a clue that the question is being asked badly. A type checker is not a quality tool or a productivity tool. It is a proof system, and it proves one specific proposition about your program: that a certain class of operation will never be applied to a value that cannot support it, on any input, on every path, including the ones you never thought of.
Once you see it that way, both the enthusiasm and the disappointment make sense. That proposition is astonishingly strong — nothing else you can do to a program gives you a statement quantified over all executions. It is also much narrower than "the program is correct", and the gap between the two is where most of the arguing happens.
AssumesYou have seen a type error and a runtime error, and noticed that they arrive at different times.
Slide 1
A proof, quantified over every run
The distinguishing feature of a type checker is not what it catches but how much it covers.
function first(xs: string[]): string { return xs[0].toUpperCase(); }typescript The checker will accept this, and in accepting it makes a claim about every call that will ever be made: whatever
xsturns out to be, it will be an array of strings. No test does that. A test tells you about the inputs it was given, and there are usually more inputs than that.The claim is also checked before the program runs, which changes the economics of finding a mistake. A type error is found in the editor, by the person who made it, seconds after making it. The same mistake found dynamically may be found by a customer, six weeks later, in a stack trace with none of that context. Both are the same bug and they cost very different amounts.
Slide 2
What a type system actually rules out
It is worth being concrete about the class of errors, because it is narrower than the reputation.
A type system rules out applying an operation to a value of the wrong shape. Calling a method that is not there. Passing three arguments where two are expected. Adding a number to a function. In a language with sum types it also rules out forgetting a case, which is a genuinely different and more interesting guarantee, and it is the one that makes people who have used it reluctant to go back.
Notice the example above.
xs[0].toUpperCase()typechecks, and on an empty array it throws, because most type systems say that indexing an array of strings yields a string and decline to mention that the array might be too short. The checker proved exactly what it promised. It simply did not promise that.Slide 3
What it cannot rule out
The list of things a mainstream type system says nothing about is longer than the list of things it catches.
function days(a: Date, b: Date) { const gap = b.getTime() - a.getTime(); return gap / 86400; }typescript Every type is correct. The function is wrong by a factor of a thousand, because milliseconds are not seconds, and no type checker in ordinary use will say a word about it. Nor will one tell you that a list is sorted, that an index is in range, that a lock is held, that money and mass should not be added, that the two branches of a conditional do the same thing, or that the function does what its name says.
Some of these are expressible in the more powerful systems — units of measure, refinement types, dependent types — and those systems exist and are used. What they cost is a much larger proof burden, borne by whoever writes the code, and that is a real trade rather than a free improvement.
Slide 4
Soundness, and the languages that gave it up
There is a technical property lurking under the word "safe", and the popular languages have taken conspicuously different positions on it.
A sound type system never accepts a program that will commit a type error at runtime. It is a strong guarantee, and it forces the checker to reject some programs that would in fact have been fine, because it cannot prove that they are. Every sound system rejects correct programs; that is the deal.
TypeScript is deliberately not sound, and says so in its own documentation. Array indexing, the
anyescape hatch and the way class fields are checked all admit programs that will fail. This was a design decision rather than an oversight: the goal was to describe JavaScript as it is actually written, and soundness would have meant rejecting a great deal of working code. Java's array covariance is a similar hole, patched with a runtime check. Whether an unsound checker is worth having is a fair question, and the honest answer is that it catches most of what it aims at and you must not treat its silence as a proof.Slide 5
Gradual typing, and what it is really for
The last fifteen years quietly ended the argument in practice, by letting one program be both.
TypeScript, Python's annotations, Ruby's signatures and PHP's types all let a codebase start untyped and acquire types where they pay. This is not a compromise between two camps so much as an acknowledgement that the value of a type varies enormously by location. At the boundary of a system — where data arrives from a network, a form, another team — a type is a description of a contract with someone who may not honour it, and worth a great deal. Deep inside a pure function that is called from three places, it is worth much less.
The pattern that has emerged is to type the boundaries heavily, validate at them, and let the interior be as strict as the team finds comfortable. It is less ideologically satisfying than either original position and it is what almost everyone now does.
Slide 6
Types and tests answer different questions
The most common bad argument on both sides is that one of these replaces the other.
A type says something weak about every possible execution. A test says something arbitrary about one. The two are not competing; they cover different axes, and a program with neither is in a worse position than a program with either. What is true is that they overlap at the margin: a language with sum types and exhaustive matching removes a category of test that a dynamic language must write by hand, and a thorough test suite catches shape errors eventually, at greater expense and later.
The interesting question is not which to have but where the boundary sits for a given system, and it moves. A four-file script does not need either. A payments ledger maintained by thirty people over eight years needs both, and needs them mostly at the edges where the shapes are least under its control.
Slide 7
The honest summary
What the evidence supports is narrower than either camp's rhetoric, and it is still worth stating.
Types find shape errors early and cheaply, and the earlier something is found the less it costs. They are documentation that cannot go stale, which is the only kind that stays true. They make large-scale change tractable, because a rename that the compiler follows is a different activity from a rename you must search for. Those are real, and they are why untyped codebases past a certain size tend to grow a type checker.
What they do not do is make a program correct, and the temptation to believe otherwise is the one genuine hazard. The most dangerous line in any typed codebase is the one where a value crosses in from outside, wearing a type somebody asserted rather than checked. Everything downstream is then reasoning carefully from a premise that was never established, which is a more expensive kind of wrong than having no types at all.
Works in the collection
The arguments above are hanging on the walls of the museum, in one form or another. These are the ones worth looking at next.
The rest of the programme
- Compiled, interpreted, and the space betweenA distinction that stopped describing anything decades ago, why it persists, and what is actually different about the machinery underneath.
- What a compiler actually doesFour jobs in a row, each mechanical, none magic: text to tokens, tokens to a tree, a tree to a judgement, and a judgement to instructions.
- Functional programming and object orientationTwo ways of arranging a program, what each one genuinely makes easy, and the trade-off underneath the argument that neither side can escape.
- The principles of object orientationEncapsulation, inheritance, polymorphism and SOLID, one at a time: what each actually claims, which held up, and which its own community abandoned.
- Null, and the mistake its inventor apologised forTony Hoare called it his billion-dollar mistake. What was actually wrong with it, what the alternatives cost, and why the fix took forty years to arrive.
- What abstraction costsAbstraction is sold as free and is not. What you buy, what you pay, and how to tell before writing it which of the two is larger.
- Immutability, and what it is not free ofValues that never change buy sharing, comparison and time travel. They are not free, and it is worth knowing where they are expensive before you commit.
- Recursion, and why it feels like cheatingA function that calls itself looks like an unpaid debt. What makes it terminate, what it costs on the stack, and why some problems resist any other shape.
- Big-O, and what it deliberately ignoresComplexity notation throws away constants, hardware and every input you will actually see. Knowing what it discards is what makes the number useful.
- What regular expressions cannot matchThere is a precise boundary around what a regex can recognise. It explains the famous refusal to parse HTML, and why some patterns run forever.
- Concurrency is not parallelismOne is a way of structuring a program, the other a way of executing it. Keeping them apart explains why async helps a web server and threads often do not.
- Errors: exceptions, values, and what each hidesThrowing makes the happy path readable and the failure paths invisible. Returning errors as values does the opposite. Neither side has won.
- Why programs are hard to changeSoftware is called soft because it can be edited. Why editing gets harder every year, what the mechanism is, and which of the usual remedies work.