The Inconvenient Truth About Dynamic vs. Static Typing

Sometimes there are these moments of truth. They happen completely unexpectedly, such as when I read this tweet:
David is the author of the lesser-known but not at all lesser-interesting Whiley programming language, a language that has a lot of static type checking built in it. One of the most interesting features of the Whiley language is flow sensitive typing (sometimes also simply called flow typing), which is mostly useful when used along with union types. An example from the getting started guide

function indexOf(string str, char c) => null|int:

function split(string str, char c) => [string]:
  var idx = indexOf(str,c)

  // idx has type null|int
  if idx is int:

    // idx now has type int
    string below = str[0..idx]
    string above = str[idx..]
    return [below,above]

    // idx now has type null
    return [str] // no occurrence

Remember, other languages like Ceylon also know flow-sensitive typing, and even Java does to a certain extent, because Java has union types, too!

try {
catch (SQLException | IOException e) {
    if (e instanceof SQLException)
        doSomething((SQLException) e);
        doSomethingElse((IOException) e);

Granted, Java’s flow-sensitive typing is explicit and verbose. We could expect the Java compiler to infer all the types. The following should type-check and compile just as well:

try {
catch (SQLException | IOException e) {
    if (e instanceof SQLException)
        // e is guaranteed to be of type SQLException
        // e is guaranteed to be of type IOException

Flow typing or flow sensitive typing means that the compiler can infer the only possible type from the control flow of the surrounding program. It is a relatively new concept in modern languages like Ceylon, and it makes static typing extremely powerful, especially if the language also supports sophisticated type inference via var or val keywords!

JavaScript static typing with Flow

Let’s get back to David’s Tweet and have a look at what the article said about Flow:
The presence of a use of length with a null argument informs Flow that there should be a null check in that function. This version does type-check:

function length(x) {
  if (x) {
    return x.length;
  } else {
    return 0;

var total = length('Hello') + length(null);

Flow is able to infer that x cannot be null inside the if body.
That’s quite cunning. A similar upcoming feature can be observed in Microsoft’s TypeScript. But Flow is different (or claims to be different) from TypeScript. The essence of Facebook Flow can be seen in this paragraph from the official Flow announcement:
Flow’s type checking is opt-in — you do not need to type check all your code at once. However, underlying the design of Flow is the assumption that most JavaScript code is implicitly statically typed; even though types may not appear anywhere in the code, they are in the developer’s mind as a way to reason about the correctness of the code. Flow infers those types automatically wherever possible, which means that it can find type errors without needing any changes to the code at all. On the other hand, some JavaScript code, especially frameworks, make heavy use of reflection that is often hard to reason about statically. For such inherently dynamic code, type checking would be too imprecise, so Flow provides a simple way to explicitly trust such code and move on. This design is validated by our huge JavaScript codebase at Facebook: Most of our code falls in the implicitly statically typed category, where developers can check their code for type errors without having to explicitly annotate that code with types.

Let this sink in

most JavaScript code is implicitly statically typed
JavaScript code is implicitly statically typed
Yes! Programmers love type systems. Programmers love to reason formally about their data types and put them in narrow constraints to be sure the program is correct. That’s the whole essence of static typing: To make less mistakes because of well-designed data structures. People also love to put their data structures in well-designed forms in databases, which is why SQL is so popular and “schema-less” databases will not gain more market share. Because in fact, it’s the same story. You still have a schema in a “schema-less” database, it’s just not type checked and thus leaves you all the burden of guaranteeing correctness. On a side note: Obviously, some NoSQL vendors keep writing these ridiculous blog posts to desperately position their products, claiming that you really don’t need any schema at all, but it’s easy to see through that marketing gag. True need for schemalessness is as rare as true need for dynamic typing. In other words, when is the last time you’ve written a Java program and called every method via reflection? Exactly… But there’s one thing that statically typed languages didn’t have in the past and that dynamically typed languages did have: Means to circumvent verbosity. Because while programmers love type systems and type checking, programmers do not love typing (as in typing on the keyboard).

Verbosity is the killer. Not static typing

Consider the evolution of Java: Java 4

List list = new ArrayList();

// Eek. Why do I even need this Iterator?
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    // Gee, I *know* I only have strings. Why cast?
    String value = (String);

    // [...]

Java 5

// Agh, I have to declare the generic type twice!
List<String> list = new ArrayList<String>();

// Much better, but I have to write String again?
for (String value : list) {
    // [...]

Java 7

// Better, but I still need to write down two
// times the "same" List type
List<String> list = new ArrayList<>();

for (String value : list) {
    // [...]

Java 8

// We're now getting there, slowly
Stream.of("abc", "xyz").forEach(value -> {
    // [...]

On a side-note, yes, you could’ve used Arrays.asList() all along. Java 8 is still far from perfect, but things are getting better and better. The fact that I finally do not have to declare a type anymore in a lambda argument list because it can be inferred by the compiler is something really important for productivity and adoption. Consider the equivalent of a lambda pre-Java 8 (if we had Streams before):

// Yes, it's a Consumer, fine. And yes it takes Strings
Stream.of("abc", "xyz").forEach(new Consumer<String>(){
    // And yes, the method is called accept (who cares)
    // And yes, it takes Strings (I already say so!?)
    public void accept(String value) {
        // [...]

Now, if we’re comparing the Java 8 version with a JavaScript version:

["abc", "xyz"].forEach(function(value) {
    // [...]

We have almost reached as little verbosity as the functional, dynamically typed language that is JavaScript (I really wouldn’t mind those missing list and map literals in Java), with the only difference that we (and the compiler) know that value is of type String. And we know that the forEach() method exists. And we know that forEach() takes a function with one argument.

In the end of the day, things seem to boil down to this:

Dynamically typed languages like JavaScript and PHP have become popular mainly because they “just ran”. You didn’t have to learn all the “heavy” syntax that classic statically typed languages required (just think of Ada and PL/SQL!). You could just start writing your program. Programmers “knew” that the variables would contain strings, there’s no need to write it down. And that’s true, there’s no need to write everything down! Consider Scala (or C#, Ceylon, pretty much any modern language):

val value = "abc"

What else can it be, other than a String?

val list = List("abc", "xyz")

What else can it be, other than a List[String]? Note that you can still explicitly type your variables if you must – there are always those edge cases:

val list : List[String] = List[String]("abc", "xyz")

But most of the syntax is “opt-in” and can be inferred by the compiler.

Dynamically typed languages are dead

The conclusion of all this is that once syntactic verbosity and friction is removed from statically typed languages, there is absolutely no advantage in using a dynamically typed language. Compilers are very fast, deployment can be fast too, if you use the right tools, and the benefit of static type checking is huge. (don’t believe it? read this article) As an example, SQL is also a statically typed language where much of the friction is still created by syntax. Yet, many people believe that it is a dynamically typed language, because they access SQL through JDBC, i.e. through type-less concatenated Strings of SQL statements. If you were writing PL/SQL, Transact-SQL, or embedded SQL in Java with jOOQ, you wouldn’t think of SQL this way and you’d immediately appreciate the fact that your PL/SQL, Transact-SQL, or your Java compiler would type-check all of your SQL statements. So, let’s abandon this mess that we’ve created because we’re too lazy to type all the types (pun). Happy typing! And if you’re reading this, Java language expert group members, please do add var and val, as well as flow-sensitive typing to the Java language. We’ll love you forever for this, promised!

16 thoughts on “The Inconvenient Truth About Dynamic vs. Static Typing

  1. It is certainly very interesting that despite the “apparently great” advantages of dynamic typing, more recently we have seen languages like Flow and TypeScript that actually try to make JavaScript more type safe.

    Definitely type inference is the best of both worlds. I recently spent some time to play with Google Go and I was fascinated by their implementation of statically typed duck typing and lots of type inference here and there that gives you this feeling that the language is more adaptable and dynamic. And awesome examples of functional languages like SML, Haskell and Scala are living proof of how great type inference is.

    In those regards languages like TypeScript actually fail for me, because in order to take advantage of the type system, they have to make the code really verbose, given the fact that the compiler can tell very little about types unless you explicitly tag them. So, this type of language actually go in the opposite direction, they are moving towards a feature statically typed languages are actually trying to hide from developers, namely: having to explicitly say the types of things.

    I don’t know anything about Flow, but sounds to me like this niche is a bit crowded already. But I will play with it for a few hours just for fun now that I read your interesting article.

    1. Thanks for the feedback. I’d love to read an article from you about Go! I haven’t gotten around to trying it out yet, but I’m intrigued by statically typed duck typing (although I believe that it is mostly a bad idea)…

  2. I prefer static typing myself but you are incorrect about there being no point to dynamic typing other than reducing verbosity. There are things you can enable in a language with dynamic typing to do that you cannot do statically.

    1. Yes and I gave those things some credit somewhere in the middle. Those 2-3 times when you need reflection, or when you absolutely need to cast, that’s fine. But it shouldn’t be the default behaviour.

      Obviously, Java is far from being a fully statically typed language, otherwise we would have never had subtype polymorphism.

        1. Very interesting indeeed, thanks for sharing. I also like the conclusion that most discussion is really about the technical inferiority of some commercial languages (in particular Java) rather than the mere comparison between “dynamic” vs. “static” typing.

          And I particularly like the reproach that people who prefer dynamic typing consider themselves dynamic, too :-) That kind of hits the spot for me with my personal experience…

    2. (7 years later…)

      > There are things you can enable in a language with dynamic typing to do that you cannot do statically.

      But which are those? Are there any that are not about being somewhat sloppy in design and implementation?

      Concretely, yes, there are things you can do in Clojure for example (dynamic LISP-like) that you cannot (elegantly) do in Java, but they are about having no union types in Java (so you have to duplicate code for handling both ints and strings) and with the fact that in Clojure data literals and code literals are written the same way, as the Gods of Computation expect (then again, what is the type of a code literal? Is it “tree of stuff”?).

      Note that a type system is not “binary”, it is a slider; it is a way of making additional statements about the code. These statements can be used by “the compiler” to make additional verifications prior to releasing the code to the hardware. “This code is meant to stay on the following rails to avoid loss of data, funds or life” says the programmer. “I can confirm that part” says the compiler. Depending on the design of the (language, type system) tuple, you can say more, or you can say less. Depending on the strength of the typing theorem prover, you can prove more, or you can prove less. If the type system is too weak, you may want to manually add an assertion or two.

      Of course you are forced to keep the typing system’s expressiveness sufficiently manageable to not kill the theorem prover with a combinatorial explosion, but that’s just the nature of the beast. This also applies to the developer who will probably balk at giving the exact type of, for example, a function transforming functions – “Typed Clojure” is not a great success, I hear. So there will be ways around the type system.

      Agda and Idris (“dependently typed”) seem to be the next step: types as first class citizens and constructible at runtime? Yes, please! We all love quick uni-typed coding (actually not so quick once you add the speccing, asserting, bloated unit testing & hair-raising debugging phases), but then suddenly, on a Saturday evening, there is IT on the phone … and then you reconsider …

  3. Interesting article. Could you point to a repeatable study that shows how statically typed languages are better than the dynamically typed languages (where better is
    some chosen criteria, like average loc per day, average number of critical bugs per month or something else)?

    1. Those studies exist, see: The Programming Language Wars: Questions and Responsibilities for the Programming Language Community

    1. Complication is a killer. Incidental (unnecessary) complexity is a killer. Verbosity is both.

      Essential complexity kills nothing. It’s an unavoidable part of whatever problem you are trying to solve. The less directly your chosen language can express it, the more incidental complexity (or complication) you are adding.

      Of course, a significant number of developers can’t actually visualise the essential complexity and so couldn’t express it anyway, so verbose accretion is their only option.

Leave a Reply