loading...

Dynamic typing is a sin

yujiri8 profile image Ryan Westlund Originally published at yujiri.xyz ・4 min read

Obviously the title is a bit hyperbolic as my own favorite language is a dynamic one, but the last few years of learning many different languages have convinced me that dynamic typing is bad and there are no good reasons for a general purpose language to use it. Note that I'm not saying there are no good reasons to use a dynamic language, only that there are no good reasons to make one (there can be good reasons to use existing dynamic languages because existing static languages are imperfect; I'm interested in design theory here, not convincing anyone to abandon their favorite dynamic language).

I think the main benefit of static typing - catching mistakes earlier so development is faster - is obvious, but there are less obvious ones:

  1. Better error messages. In a dynamic language, not only do you have to run the program to find a type error, but when you do, the error usually won't point to the line that actually contains the mistake.

  2. Code analyzers. Linters and vetters can never be very good in dynamic languages, because so much of the information necessary to determine what's a mistake isn't available until runtime. Dynamic languages use features like constructing types and changing what names are defined in a namespace at runtime. This makes it impossible for a static analyzer to not occasionally turn up false positives.

    This is why I don't use mypy, as much as I appreciate the idea of it. When I set it up on my website's codebase, it found 18 'errors', none of which indicated a problem.

  3. Documentation. Without type signatures, we have to document what a function expects in another way. Type signatures, since they're formal, are easier to parse (for the same reason bulleted lists make information easier to parse) and aren't easy to forget to write. Who's had the experience of reading a Python function's documentation that just doesn't say what it expects or returns?


I think all the supposed cons of static typing are really just cons of bad type systems, and while I don't know any existing language that avoids them all, we can easily imagine a language that combines:

  • Generics / parameterized types (eg. a function can take an "array", without specifying an element type, and work on arrays on any type)

  • Sum types (a type that contains one of multiple other types)

  • Interface-based polymorphism (two types need not know about each other to both be usable in a context that only uses their common properties)

  • Inheritance (Go has the best incarnation of it)

  • Type inference (types are inferred by the compiler so you rarely have to write them)

With several clear benefits of static typing, I address counterarguments.

Serialization

In a static language, you can't easily deserialize data of unknown structure, such as a JSON object.

First, almost all use cases of deserialization involve expecting a specific structure, like a Customer or the arguments to post a Comment. I have yet to find one that doesn't. But for those that don't, powerful type systems still have ways around this, like combining maps with sum types. Consider this Rust example:

use std::io;
use serde_json::Value;

fn main() -> Result<(),io::Error> {
    let data: Value = serde_json::from_reader(io::stdin())?;
    match &data {
        Value::Null => println!("You entered null"),
        Value::Bool(b) => println!("You entered {}", if *b {"yes"} else {"no"}),
        Value::Number(n) => println!("You entered {} more than 2", n.as_f64().unwrap() - 2.),
        Value::String(s) => println!("You entered the text {}", s),
        Value::Array(a) => println!("You entered the values: {:#?}", a),
        Value::Object(o) => println!("You entered the properties: {:#?}", o),
    }
    Ok(())
}

In each branch, I have access to the data decoded as that type. I didn't have to handle them all either - I could've used a default case to fail for types I didn't want to handle.

The o in the Object branch, by the way, is of type Map<String, Value>, so this works recursively.

When the system is wrong

Sometimes the human knows more than the computer. No matter how smart the type checker is, static typing prevent things that a human can tell would provably work.

Good static languages have ways to assert such a requirement and panic if it isn't true. In Rust, there's unwrap; if I have a of type Option<Thing> (which can be either None or Some(Thing) and normally requires me to handle the None case before I can get the Thing out), it has an unwrap method that returns the internal Thing and panics if it was None.

Something similar exists in Haskell with fromJust, which takes a Maybe type (which is a sum type of Nothing and Just a) and gives you the a unwrapped, crashing at runtime if you were wrong and it was Nothing.

And neither of these are language primitives; they're library code you could implement yourself.

Dynamic field access

I've sometimes taken advantage in dynamic languages of the ability to access a field of an object not by name, but by variable name. In Javascript, if I might want to access many different fields of customer, I can do customer[field] instead of:

switch field {
case "name": doThing(customer.name)
case "email": doThing(customer.email)
case "phone": doThing(customer.phone)
case "website": doThing(customer.website)
case "notes": doThing(customer.notes)
}

But some static languages can solve this too; Stackoverflow users give a Go example and a Java example. Both of these are much more verbose than how Javascript can do it, but they didn't even have to be that. They didn't even have to be implemented via runtime reflection: although I don't know any static languages that do this, one could allow deriving an enum type of a struct's fields and casting a string to that, in a way that it would error at runtime if the string wasn't the name of one of the fields.


Given the above, I find the case against dynamic typing pretty overwhelming. If you think there are any benefits left, point them out and let's see.

Posted on by:

yujiri8 profile

Ryan Westlund

@yujiri8

I'm a programmer, writer, and philosopher. My Github account is yujiri8; all my content besides code is at yujiri.xyz.

Discussion

markdown guide
 

But some static languages can solve this too

That is a problem, the one programming language you use must be to do MOST that you wanted.

Though I do agree that newer programming language design could be nice.

data of unknown structure, such as a JSON object.

Currently, I prefer to write JSON schema for everything, that is, nothing is unknown, and it should throw error, if it doesn't match.

Sometime it involves anyOf and additionalProperties as well, and it depends on the language on how well supported it is. (I generally set additionalProperties: false, if possible.)

JSON schema isn't well supported for instanceof Class, or some language specifics, though.

Both of these are much more verbose than how Javascript can do it, but they didn't even have to be that.

they're library code you could implement yourself.

If more verbose means possibly more bugs, or development hardship, or possibly hard to maintain. That is a problem.

 

That is a problem, the one programming language you use must be to do MOST that you wanted.

If more verbose means possibly more bugs, or development hardship, or possibly hard to maintain. That is a problem.

These are good concerns in general, but again I'm talking about design theory rather than existing static languages versus existing dynamic languages. My goal isn't to convince anyone to abandon dynamic languages.

 

I very much believe that dynamic language is the wrong choice most of the time. You mentioned examples of static languages solving a problem with them being static, but I think that is to stay within the type system. If you just leave the types behind your no worse off than dynamic.

The challenges I generally face is trying to keep my architecture within the type system, avoiding casts/type changes. Generally D gives me tools to keep the type system verifying my code at compilation, when I try to achieve something similar in C# a cast is required.

 

I had so many discussion about this in the past... but seems like developers are finally seeing the light. Nonetheless, I see many software engineers stuck in maintenance nightmares, because someone in the past decided that python was the best to develop a code product with thousands of code lines. I rarely see anyone on these projects refactoring or improving anything, they are to scared to mess with the code. There's always an insufficient number of unit tests. Nowadays, in the opposite fence, I can refactor code fearlesses in Rust. Better, I almost program in Rust by throwing random lines of code and wait for the compiler to complain. I get so lazy with these tools...

Relating to the "Dynamic field access". Kotlin has a nice way of referencing a field and do something like customer[this::field], although not a string. Also, meta-programming features such as proc_macros in Rust and Scala 3 macros can make many of these features possible.

 

At the beginning of the massive use of static typing, with C and C++, developers believed that, if it can compile, it can be shipped. Then, it became a joke. 'cause it doesn't work that way.

In my view, a compiler will throw you the very basic problems of your software. No more. Static typing is not a silver bullet, and it's not useful in every scenario. But it can add overhead. In short, the need to precise the type (almost) all the time, the need to create generics which is a hard problem. When you look at Go, it's a language which want to be simple but sill mix interface constructs with generic in a very... interesting (and confusing?) way.

What about exploration? It's difficult to get a design right the first time, and it's even more difficult when the codebase is growing. Dynamic typing is good for exploration. You don't have to change 32 types to try something new. To me, it's a big argument, because we work iteratively (hopefully).

About your arguments for static typing:

  • Better error messages. Interpretation is mainly compilation nowadays, so it's not really true anymore. Yes, a statically typed language (if it's a thing) will get more of them. But I'm not sure it's much more.

  • Code analyzers. That's true, but let's not forget that, even for a static typing, many things still happen at runtime.

  • Documentation. If I need types to understand what a function / class / whatever does, I think there is a problem somewhere. The function is not well named, or is not in the good package. Nevertheless, the problem is not the type.

Don't get me wrong. I like static typing. But I forced myself to work with dynamic language to see what I was missing (Clojure / Scheme lately), and I begin to see some advantages.

 

In my view, a compiler will throw you the very basic problems of your software. No more. Static typing is not a silver bullet, and it's not useful in every scenario. But it can add overhead. In short, the need to precise the type (almost) all the time, the need to create generics which is a hard problem. When you look at Go, it's a language which want to be simple but sill mix interface constructs with generic in a very... interesting (and confusing?) way.

It's true it's not a silver bullet. Overhead of checking the whole program is something I heard raised recently... but that's only really a downside if the statically typed language is interpreted (which is rare and for understandable reasons). For a compiled language, the type checking will only be done at compile time, not every time it's run.

I don't think creating generics is a hard problem. Go doesn't have them yet, but languages likes Haskell and Rust have particularly elegant solutions, where generics seem like a natural extension of how things work than some added feature you have to learn.

I think Go's interface system is quite enlightened. The only confusion I can relate to either stems from the nil issues or I suppose if you try to use interfaces to get around the lack of generics (which is a hack, and hopefully won't be necessary come Go 2).

What about exploration? It's difficult to get a design right the first time, and it's even more difficult when the codebase is growing. Dynamic typing is good for exploration. You don't have to change 32 types to try something new. To me, it's a big argument, because we work iteratively (hopefully).

I think it's exactly the opposite of this: I fear refactoring in a dynamic language much more, because odds are I'll break something and I won't find out. Changing a type usually means changing semantics too, which means changing the places its used even in a dynamic language. At least in a static language, the compiler can make sure I don't miss any of them. Changing explicitly written type signatures when nothing else about the area needs to change can be cumbersome, but I'd rather that than have no assurance that I'm not changing the type (and even that's mostly solved by type inference).

Better error messages. Interpretation is mainly compilation nowadays, so it's not really true anymore. Yes, a statically typed language (if it's a thing) will get more of them. But I'm not sure it's much more.

I'm not sure what you mean by "interpretation is mainly compilation nowadays"?

Documentation. If I need types to understand what a function / class / whatever does, I think there is a problem somewhere. The function is not well named, or is not in the good package. Nevertheless, the problem is not the type.

I think this generalization is too hasty. A few examples:

  • Is it always clear from good naming whether something takes a unicode string or a bytestring?

  • Python's random.choice works by taking len, generating an int, and indexing, which means it doesn't work on non-indexable types like set and dict even though you might expect it to since picking a random element is something that makes sense for those types. But there's no type signature to make it obvious that random.choice doesn't work with them. And, again, if you pass such a type, the error message you get shows the line in the random module's source. (Worse, with dicts it can exhibit extremely confusing errorless behavior if the index happens to be a valid key)

    And while the function's documentation could just be clearer, or it could be argued it's clear enough already (but wasn't for me), it would be nice to free the author from the burden of always worrying about whether the English documentation is clear enough.

  • Have you ever worked with a library that had different types representing different but related things? For example, at my job we have a Config and a ConfigData struct. ConfigData represents values read from the TOML config file, and Config represents the actual database config (they have different properties for good reasons). It's not so much that the structs are poorly named, but that the concepts are so similar that it wouldn't be obvious without type signatures which one a function expects.

 

When I say "interpretation is mainly compilation nowadays", I mean that dynamically typed languages (in general) are compiled to bytecode, which already catch many errors.

I think Go's interface system is quite enlightened

I was speaking about the implementation of generics in Go, which uses interfaces. Here it is: go.googlesource.com/proposal/+/ref...

I saw many developers using interfaces like it's a magical tool which will decouple your code wherever you put them. It's a concept which is not well understood IHMO, and now I'm curious to see how it will go with generics on top.

Is it always clear from good naming whether something takes a unicode string or a bytestring?

This look like an edge case to me.

Python's random.choice

I never wrote some Python, but let's say that it's statically typed. How do you implement your random choice? With generics? in that case, you have the same problem, isn't it?

Have you ever worked with a library that had different types representing different but related things? For example, at my job we have a Config and a ConfigData struct. ConfigData represents values read from the TOML config file, and Config represents the actual database config (they have different properties for good reasons). It's not so much that the structs are poorly named, but that the concepts are so similar that it wouldn't be obvious without type signatures which one a function expects.

It's difficult to say without seeing the code. But only the names raise many questions to me. The types won't really answer them.

It's an interesting discussion. I just think we put too much faith in static typing, that's all. It shouldn't be a "solution" for bad naming, for example. It's useful, I agree, but it's not a life savior.

it would be nice to free the author from the burden of always worrying about whether the English documentation is clear enough.

I don't think we could "free the burden" as you say. A big part of our work is documentation. I speak about documentation in a large sense, and I don't think type add so much in this regard. To me, tests, naming and comments (when needed) are way more important than types.

About refactoring: we need to write automated test anyway at the end of the day. This is the real security net to me, not typing. With or without type, I won't feel comfortable to refactor without tests anyway.

When I say "interpretation is mainly compilation nowadays", I mean that dynamically typed languages (in general) are compiled to bytecode, which already catch many errors.

Ah, I see. But they don't catch type errors in compiling to bytecode. If they did, they would just be statically typed languages.

I don't think we could "free the burden" as you say. A big part of our work is documentation. I speak about documentation in a large sense, and I don't think type add so much in this regard. To me, tests, naming and comments (when needed) are way more important than types.

I agree that documentation is important, and I think I also agree that good documentation makes type signatures unnecessary for that purpose... but I think it's still useful to have types for this because in reality, we won't always write good documentation. . I see types as sort of a safety net in this regard, I guess.

On the other hand... an argument could be made that this causes a bad incentive for documentation. Haskell is a language with a powerful type system and generally sub-par documentation. I've heard some community members remark that "The Haskell community doesn't understand that type signatures aren't a substitute for documentation". But other statically typed languages don't seem to have this problem. Go in particular has easily the best documentation of any language I've used, even for third-party libraries.

I never wrote some Python, but let's say that it's statically typed. How do you implement your random choice? With generics? in that case, you have the same problem, isn't it?

Only if you just take Python and add static checking while otherwise keeping the same type system. In a language designed for it, like Rust or Haskell, I could restrict the container type to "something that supports len() and indexing".

About refactoring: we need to write automated test anyway at the end of the day. This is the real security net to me, not typing. With or without type, I won't feel comfortable to refactor without tests anyway.

I've heard this also, but tests will never cover every possibility. (Even what's often called '100% test coverage' doesn't mean there aren't cases that haven't been tested.) In practice you really want both.

 

I'm torn on this subject. I've switched from JavaScript to TypeScript because it gives much more confidence when I'm working on code.

But my favorite TypeScript feature is that it can turn TypeScript off. This is useful when I need to work especially quickly, or I have a single function that would be tricky to add types to, but I have confidence in that function's code will behave as expected. I really like incrementally adoptable types, and I hope to see more of the existing dynamically-typed languages move towards this direction.

 

Dynamic typing may suck for object oriented programming languages, but for functional programming languages, it is at least on par with static typing. It cuts out a ton of crap and boilerplate. And on the topic of Maybe, well... "Maybe Not": youtube.com/watch?v=YR5WdGrpoug