Why I prefer dynamic-typing over static-typing: the speed of adapting to change

(written by lawrence krubner, however indented passages are often quotes). You can contact lawrence at: lawrence@krubner.com, or follow me on Twitter.

I am a fan of dynamic typing. So is corporate America. The widespread use of PHP, Ruby, Python and Javascript suggest that dynamic typing is useful. Static typing tends to be used in areas where legal regulations create needs that outweigh programmer productivity. If static typing lead to greater programmer productivity (via a reduction in bugs) then corporate America would only use statically-typed languages. But it doesn’t.

In How ignorant am I, and how do I formally specify that in my code? I said that I liked to add run-time contracts to a function as I better understand it. When I first write a function, I may not know for sure how I will use it. In my mind, the architecture is still in flux. Adding contracts comes later, when I am confident of the design. Perhaps this approach only appeals to developers who are often creating ad-hoc architectures?

There are many arguments in favor of static-typing. Once you have confidence in your architecture, then it is good to lock it down with contracts that warn about breaking changes. I imagine there are many developers who only do specific kinds of programming (CRUD apps for a database, “business intelligence” reports and dashboard, etc) and in those cases they use one architecture for their whole careers. In that case, perhaps they have a good reason to use static-typing. They are already confident with the architecture, as they have used it a dozen times before.

For the programming that I do, I am often creating new architectures. Therefore I need dynamic typing.

This is a standard defense of static typing:

“A large class of errors are caught, earlier in the development process, closer to the location where they are introduced.”

I would re-state this as:

“A large class of errors are introduced, which otherwise would not exist.”

I have dealt with terrible APIs that were outside of my control. When I worked at Timeout.com, we had to pull in data from Ticketmaster and also Booking.com. These APIs were inconsistent, and they would change over time. They also left out fields, so all fields had to be treated as an optional, which it made it difficult to enforce a meaningful contract.

Consider dealing with JSON in Java. Every element, however deeply nested, needs to be cast, and miscasting leads to errors. Given JSON whose structure changes (because you draw from an API which leaves out fields if they don’t have data for that field) your only option is to cast to Object, and then you have to guess your way forward, figuring out what the Object might be.

Consider the Salesforce clone of Java, Apex, which I have had to work with.

The if() statements here are the same one’s that I would have to write in Ruby or Python or PHP, but meanwhile I’ve had to do a bunch of other, useless work:

    public Object deserializeJson(String sandi_data) {
        Object objResponse = JSON.deserializeUntyped(sandi_data);
        
        if (objResponse instanceof Map<String, Object>) {
            Map<String, Object> mapResponse = (Map<String, Object>)objResponse;                
            List<Object> dataList = (List<Object>)mapResponse.get('data');
            if(dataList == null) {
                String err = 'The Sandi API field for data was null';
                System.debug(err); 
                ApexPages.Message msgErr = new ApexPages.Message(ApexPages.Severity.ERROR, err);
                ApexPages.addmessage(msgErr);
                return null;
            } else if (dataList.isEmpty()) {
                String err = 'The Sandi API field for data was empty';
                System.debug(err); 
                ApexPages.Message msgErr = new ApexPages.Message(ApexPages.Severity.ERROR, err);
                ApexPages.addmessage(msgErr);
                return null;
            } else {
                System.debug('dataList:');
                System.debug(dataList);
                return dataList; 
            }
        }
        return sandi_data;
    }

And then, downstream of this:

            List<Object> dataList = (List<Object>)deserializeJson(sandi_data);

            for(Integer i=0; i < dataList.size(); i++) {                
                Map<String, Object> dataMap = (Map<String, Object>)dataList[i];
                String response = fetchCompany(dataMap);
                SearchResult__c profile = saveProfileResult(response);
                cr.add(profile);
            }

I’m leaving out the code that is downstream of this function, but it is full of more of the same: guessing at fields, guessing at how they should be cast, using if() to guard against null or empty. Tons of unnecessary bloat. Lots of easy errors to make.

Again, some of the if() statements need to be made in Ruby or Python or PHP, but the rest of it is just pure bloat. Verbose, unneeded and unhelpful.

In a dynamic language I could simply work with a deeply nested data structure of maps and lists, and I’d handle the casting at the very end of the process. In a dynamic language, I could treat everything as a string till the very end, and then cast to integers or dates or floats or strings as needed. In a dynamic language, I could write the code faster, with less errors, and with less code.

Static typing does not live up to its promises. If it did live up to its promises, we would all use it, and the fact that we don’t all use it suggests how often it fails.

Since I am so often misunderstood on this subject, I hope you’ll forgive me if I indulge in a certain amount of redundancy:

To be clear, this argument applies when we have no control over the API that we draw from. We are drawing from the API of a different company. I wish they didn’t use JSON. If they have to use JSON, I wish they at least enforced a consistent schema. But they don’t. And that is why static type checking fails: because the real world is chaotic, and when you have to interact with that real world, you are often forced to do so dynamically, because of the mistakes that other companies have made. The real world is dynamic. It is full of mistakes. Dynamic typing recognizes this, much more so than static typing.

The idea that you can know an external API perfectly is a fantasy. The real world is messy. The real world does not always conform to a strict schema.

The notion that An External API Is Reliable is as stupid as the notion The Network Is Reliable:

https://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/

Many people feel that it is a serious mistake to wait to cast data. The argument is something like: “Using dynamic typing will just hide the problems under the rug, and they will explode in your face later on. Static typing makes those problems explicit at the beginning.”

What I wrote was:

“In a dynamic language I could simply work with a deeply nested data structure of maps and lists, and I’d handle the casting at the very end of the process”

I’ll simplify this: there are 3 times when we can enforce a schema:

1.) when the API call returns with a string

2.) on every line, scattered through dozens of functions

3.) at the end, when I have the data that I want

I advocated for #3. Here are the reasons I don’t like the first 2 options:

#1 – the external API is bloated, so writing a schema for the whole thing would be difficult to justify in terms of business. We only need a tiny slice of the data. More so, when the API ambushes you with an unexpected change, you have a lot of work to do to adapt to the change.

#2 – having casting discovery information scattered through dozens of functions makes the code brittle and refactoring difficult. More so, when the API ambushes you with an unexpected change, you have a lot of work to do to adapt to the change.

With Ruby or Python or PHP or any dynamic language I have the option of #3: grab the data, cast everything as a string, grab the tiny sliver of data I actually need, and then enforce the schema on that tiny sliver. This is the data that I can cast to integers, floats, dates, etc — whatever is actually needed. More so, when the API ambushes you with an unexpected change, you only need to change the casts you have in the final stage. And this is the strongest argument for dynamic typing: the minimal amount of work needed to adapt to change.

In static-type languages such as Java, I’m forced to go with either #1 or #2, and they are both bad options.

When I first wrote about this, a number of people responded by saying the problem was with JSON. On Hacker News I saw the comment “It is only the usage of an awful JSON library and a not so nice language that makes this a problem.”

But bad JSON is part of the real world. If your static-type language can not handle bad JSON, then it can not handle the real world. That is my point: static-type checking is too academic, too pure, for the real world.

As to “not so nice language”, this is an example of the No True Scotsman fallacy, which goes like this: no True statically typed language would be this bad! Somewhere there is a True statically-typed language of such beauty and purity, it overcomes all of these problems!

The real world is messy and full of mistakes. Dynamic-typing adapts to this much better than the academic purity of static typing.

[ [ UPDATE 2019-02-20 ] ]

See the conversation on Hacker News

.

Post external references

  1. 1
    https://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/
  2. 2
    https://news.ycombinator.com/item?id=19195720
Source