A Totally Polished and not-at-all half-baked Take on Static vs Dynamic Typing
I'm back from Madfest! Since this is a newsletter computer things and not a newsletter about juggling things, I won't say anything about it. Except I got to see a guy eat a banana. Without peeling it first.
Anyway, every six months or so the Tech internet turns into a giant pissfight about static vs dynamic typing. This time it was started by a very good post called No, dynamic type systems are not inherently more open, by Alexis King. King is a PLT (programming language theory) researcher and an expert in both static and dynamic languages, so the post is very well thought-out and definitely worth reading. But then it hit HN/Reddit/Lobsters and people responded with dynamic typing Hot Takes, then people responded to those people with static typing Hot Takes, and blah blah blah everybody's throwing rocks at each other.
Anyway I've shared my own Hot Takes in the past, but haven't really put them in one place for fear of drawing the ire of Jerks With Rocks but if I share them on a newsletter some of the Jerks With Rocks might subscribe after throwing rocks and my self-worth is directly tied to how many subscribers I had. So let's take a rock to the face!
Disclaimer: I don't know anything about type theory, haven't used powerful static type systems in anger, and haven't bothered to edit this. Caveat emptor.
Static Typing and Dynamic Typing are orthogonal
Static typing means that types exist as part of the syntax, dynamic types means that types exist at runtime. Most languages have one or the other, but some have both (F#) and some have neither (Bash). The thing is that the main use of each is to prevent type coercion, so there's not as much advantages to having two as there are to having at least one.
Most arguments are actually about static vs no static typing
The usual argument for dynamic typing is that static typing adds too much overhead or isn't expressive enough. But these aren't arguments for dynamic typing, they are arguments against static typing. Arguments for dynamic typing would involve showing what we can do with runtime type manipulaton. I played with this a bit in my essay Python Negatypes, where I used dynamic typing to create the type of all things that aren't iterable. Other things: reassigning the types of things at runtime, dynamically generating new types based on configuration or database layout, dynamically modifying other classes based on introduction of new classes...
In addition to misuse of dynamic to mean "not static", we also have
A lot of terminology is messily used
I'm thinking in particular of using "strong typing" to mean "static typing". Usually people mean "strong typing" to mean "no implicit type conversions", which is common even without static types. Or "static typing" to mean "any kind of static analysis". Or equating "functional programming" with "static typing".
Also a lot of static typing advocates equate "a good type system" with Haskell. So there's not much discussion of, like, how awesome parameterized modules are.
The benefits of static typing are known, the benefits of dynamic typing are unknown
Static type systems are mostly concerned with soundness: if it type-checks, there will be no type mismatches in runtime. Not all type systems are sound (Typescript isn't), but that's usually the primary benefit. This means that static typing
- Catches errors at compile time
- Documentation
- Tooling
The last two are very interesting, because they're actually consequences of the first. The tooling isn't based so much on the types themselves that the fact that the types are statically checkable annotations. Most languages use comments as annotations, which can be incorrect. But if you embed the annotations as types, you can directly check that they match the code. Similarly, documentation can fall out of sync, but documentation embedded as types will not fall out of sync.
Funnily enough, we see the same benefits with types-as-documentation in software contracts, or at least it's claimed as such.
By contrast, we don't really do a lot with dynamic types that isn't just "not having to use static types". It's also a lot harder to think of how to use it, which might be part of the problem. I sort of imagine that dynamic types are better for interactive programming, which is itself problematic because of how little we do interactive programming, too.
A consequence of this is
The current disparity between the styles is coincidental, not essential
An essential difference is one that is would always be true regardless of what we do. The difference between static and dynamic languages is coincidental: we have invested a lot more time and resources into researching good static languages than good dynamic languages. This is probably because the potential wins in static languages are obvious and easily expressable, while the potential wins with dynamic types, beyond "not having to use a static language" are... let's say "exotic".
Existing dynlangs are mostly dynamic to not have to be static. I'm honestly not familiar with any languages that push the boundaries of dynamic types. Probably Racket. Pharo, maybe? Arguably Frink, if anybody ever paid attention to Frink.¹ So we really don't know what it would look like if some people really buckled down and saw what was possible with runtime types. By contrast, we have a lot of really cool research into what we can do with static types, and even some production languages that come out of that research.
It's likely that the difference in tooling is also coincidental. Smalltalk and Lisp were both famous for having amazing IDEs.
Nobody ever gives any goddamn examples of anything
This is really annoying. Like, if I say
Static type systems can't represent all programs
Then I should give an example of something that's hard! I think 90% of the reason people got mad at Rich Hickey's "Maybe Not" is because he actually gave an example of something unrepresentable in Haskell: function subtyping. Maybe a -> a
is a subtype of a -> Maybe a
: anything that typechecks with the latter should typecheck with the former.² Another example: representing graphs as recursive structures isn't doable in rust; you gotta use adjacency matrices. A third example: the table adverb in J, which is incredibly powerful and a nightmare to type.
Similarly, I regularly see the claim
Static types are great documentation
But nobody ever gives a good example of this, either. My two favorites:
- Is a third party library able to return a nil value? If the output type is
Maybe Foo
, then it can, otherwise it can't. - How do you search a mailbox for emails with a given title? Filter the library by all types that include both
Mailbox
andRegex
in the type signature.³
One more:
Polymorphic types give you theorems for free
The example people use is a -> a
must be the id
function! Hooray. I'm always running into a -> a
and thinking "what could this BE?!". A better example: [a] -> [a]
cannot introduce distinct elements: ie for any f: [a] -> [a]
, we have set(f(l) ⊆ set(l))
. Not realizing this actually bit me in practice before.
There's (not yet) good evidence that static typing reduces bugs in practice
There's some anecdotal evidence, but some anecdotal evidence against it. And all of the studies have either not found a correlation or have had severe flaws. Real-life systems are complicated, and lots of different factors can affect what helps or hinders. Like maybe not having static types means you are naturally more careful? Look we don't know very much.
I suspect that there are cases where the type system makes representing specific ideas significantly more complicated in a way that leads to a more complex codebase, but I have no backing evidence. A good way to study this would be to look at how codebases change when you introduce gradual type systems like Mypy or Typescript.
There's also no essential reason why static typing is better. You could argue that static typing lets you formally prove code correct, but you don't actually need a statically typed language. ACL2, for example, is a formally verifiable lisp.
Most benefits and drawbacks are about specific type systems.
Whenever people points out a limitation in a specific type system, someone will inevitably shout "but this OTHER type system can do that!" They're taking the union of all the best parts of all type systems and intersecting the worst parts of them all. In reality, you choose a language and you get it's type system. OCaml having a module system doesn't help me if I'm writing Haskell.
I think this is a lot of the draw of people working in non-static languages: they need to do several different things, each of which rules out a specific language.
On the other hand, you also can't say "this static system has this issue, so all static languages are bad". That's how you get people thinking all static typing does is avoid you adding strings to integers. I'd go as far as to say that most people who prefer non-static languages haven't used anything in the ML or Haskell families.
There are a lot of really bad arguments on both sides.
Some of them!
Static typing can't handle open-world systems
See Alexis King's essay up there for a better treatment of why this isn't true.
We don't need static typing because tests catch everything
As Robert Martin infamously put it:
I don’t have to write the test for nil because I know that nil will never be passed. I don’t need to make states unrepresentable, if I know those states will never be represented.
Because code always does exactly what I expect it to at all times, and I always know all possible values third party libraries return, and I never modify code in the future in a way that changes the type signatures, and-
We don't need tests because we got static types
You know which language first got a Property-Based Testing library? Haskell.
It is true that some unit tests don't need to be written, because they'd just check stuff that is covered by the type system.
Dynamic Typing is actually static typing, just with one type
Also called "unityped". This comes from Bob Harper's "Dynamic Languages are Static Languages". Everything is of type "Type", so static languages are richer. I mean it's kinda technically correct, but it's also technically correct that languages without dynamic typing need a second program to check them and make sure it won't accidentally add an int to a string, since there's no runtime information to save you!
Also, practically speaking, everybody I've seen use either argument was kinda a smug jerk about it. Like nobody calls dynamic languages "unityped" and then doesn't strawman dynlangs.
Dynamic types are unethical
Popularized by Paul Snively and Amanda Laucher's StrangeLoop talk. The idea is that since static types are "obviously" more correct, not having them means you've got more bugs, and are more unethical.
Sleep deprivation has one of the biggest impacts on our ability to write good code. We know this. If you stay up late playing a video game, you are literally killing children.
Or maybe it's silly to call language features "ethical".
I think this covers everything except my thoughts on emulation as a concept, which aren't baked enough even for this braindump and honestly deserve a full-on polished essay treatment. In summary:
- Static typing and dynamic typing are not mutually-exclusive
- Most arguments are actually about "static types" versus "no static types"
- The biggest mistake staticlang advocates make is taking the best of all possible typesystems
- The biggest mistake dynlang advocates make is not bothering to actually explore what "dynamic" means
- For the love of god give better examples when discussing ideas
- Don't be a Jerk With Rocks
In conclusion, write more comments.
¹ I'd call Frink's measurement system a case of dynamic typing. Most static languages have a typing library that would kneel over and die if you tried to convert between "micro kilometers" and "half decafurlongs".
² Depending on how you think about Maybe
. If you consider Maybe a
as a ∪ {Nothing}
then any f: Maybe a -> a
accepts a strict superset of values and outputs a strict subset of values as a -> Maybe a
. Haskell doesn't do this because 1) it treats Just 2
as a distinct type from 2
and 2) function subtyping adds a lot of complexity.
For example, function subtyping plays hell with parametric polymorphism. Maybe a -> a
is a subtype of a -> a
. That means we lose the theorem that the only f: a -> a
is id
!
³ Okay maybe this won't find something because you actually need to be searching "String". But you get the idea!
If you're reading this on the web, you can subscribe here. Updates are once a week. My main website is here.
My new book, Logic for Programmers, is now in early access! Get it here.