Static Types vs Dynamic Types. Stop fighting and make my life easier already

17 October 2019

A look at how both statically and dynamically typed languages have pros and cons and how we should identify the benefits and choose languages that provide the best of both worlds.

There's been a doings-a-transpiring over at the old Instil place these last few weeks. The training team have been getting into a tizzy, proselytizing the relative merits of dynamic vs static languages.

First, my colleague Ryan Adams made great points promoting good software development practices above relying on static typing and showed the elegance of something like Python over Java, in his piece Static types won't save us from bad code. Then our Head of Learning, Garth Gilmour, came back with a great argument for strong static types in a functional programming paradigm using Arrow, in his piece Why Strong Static Typing Is Your Friend. These were both great posts and if you haven't checked them out be sure to do so.

Rabbit Season - Duck (Typing) Season

I'm going to argue for some of the more simple and pragmatic benefits that statically typed languages have over their dynamic counterparts. I'm also going to advocate that there is a middle ground where we can have the best of both worlds.

Static and Dynamic - What are the Benefits

I love writing code. I think it's great that as a profession we get paid to do something that is actually fun. But not all languages are created equal. Some languages (ahem, Java) make you jump through hoops and have lots of "busy-work" or boilerplate code. It's in this context that dynamic languages can seem very appealing.

But modern statically typed languages like Kotlin, TypeScript and even older languages like C# allow you to be quite succinct while retaining the benefits of statically typed languages.

So, let's start by looking at the benefits of both paradigms without picking a favourite. You will see that a pro for one method is quite often a con for the other (in the opposite form).

Benefits of Statically Typed Languages

Protection from Runtime Errors

This is the main benefit of statically typed languages. Many runtime errors become compile time errors as the compiler ensures that you are writing 'correct' code. This leads to a much smoother development experience. Clicking through a few screens of an app to find a bug, attempt a fix, re-run through the screens to find you hadn't quite fixed it is a slow and frustrating development process. IDEs highlighting in red immediately that you are doing something wrong is much more appealing.

Of course, you can still write terrible code that has the wrong behaviour, but a lot of issues can be found. Non-nullable types help avoid a whole class of errors, unfamiliar APIs must be used correctly, confusion and typos in names is avoided (is it lower, toLower, toLowerCase, toLowercase??) and many more.

Compile time protection

Ryan (and Uncle Bob) advocated more tests as a way to catch these issues and this is certainly true and a good methodology. Other methodologies such as pair programming, clean code, refactoring and others can and should be used as well and all can be used in a static world.

But what language am I writing those tests in - how many silly mistakes will I make - how much assistance do I get in authoring - what about as I refactor my code. Also, unit tests are difficult around the periphery - UI, multi-threading, IO. Finally, the lazy part of my brain nags at me for writing tests that a compiler would make redundant. I'd rather write tests for things the compiler can't figure out. I still want static typing.

IDE Assistance

When the IDE knows more about your code, you get assistance in many forms. Compare the auto-complete in WebStorm below for JavaScript (left) and TypeScript (right).

Autocomplete comparison

The errors in usage above are underlined in red like spelling mistakes in a word processor. Also, navigation goes directly to the specific implementation as there is no ambiguity. Refactoring identifies all usages accurately while with dynamic languages this can sometimes be little more than a text search and replace.

Performance Optimisation

With statically typed languages the output is typically a lower level format (native machine language, Java ByteCode, .NET IL etc). So, after compilation the compiler will have guarantees about the shape of data structures, the existence of methods etc. so the code typically performs faster than interpreted dynamic languages. In dynamic environments every instruction requires more runtime checking with a performance cost. For example, here is a benchmark comparing Python 3 vs Java

Note, performance comparisons are notoriously difficult to do fairly. The kinds of applications being built, the hardware, the language features being employed and the runtime environments have a big impact on the output. For example, a lot of Python packages have fast C implementations that can be consumed easily from Python to produce very fast programs.

Also, the potential to create fast programs is not the same as the ease for the average developer to create fast programs in a reasonable amount of time.

Expressive Language Features

The compiler can use the type system to provide language features that are more expressive and succinct. For example, Kotlin's trailing lambda combined with receivers can be used to create type safe DSLs. For example, TornadoFX uses this to define views.

// Note, this may not look like it but this is
// statically typed Kotlin with auto-complete, type checking etc
init {
  title = "Register Customer"

  with (root) {
    fieldset("Personal Information", FontAwesomeIconView(USER)) {
      field("Name") {
        textfield().bind(customer.nameProperty())
      }

      field("Birthday") {
        datepicker().bind(customer.birthdayProperty())
      }
  }

Taken from https://github.com/edvin/tornadofx/wiki/Forms

Kotlin Extensions allow built-in types and third-party types to be extended with new methods that can be used to create fluent DSLs.

// 'isValid' and 'toHash' are functions extending to the String type
if (myPasswordString.isValid()) {
    val hash = myPasswordSgring.toHash()
}

Benefits of Dynamically Typed Languages

Less Boilerplate Code

Generally speaking, dynamic languages are more succinct than their statically typed counterparts. Type annotations, generics etc. all add to the verbosity of the syntax. Languages like C# & Java require quite a bit of code before you can write any useful code.

// Java Hello World
package helloworld;

import java.util.Scanner;

public class HelloWorld {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Please enter your name: ");
        String name = scanner.nextLine();
        System.out.println("Hello " + name);
    }
}
# Python Hello World
name = input("Please enter your name: ")
print(f"Hello {name}")

Fast Development Cycles

Dynamic languages are usually interpreted (with some pre-processing for optimisation) so it is fast to make changes and then immediately run the updated program. The program will also run even with errors so that you get immediate feedback.

This can even be done in production environments as the source may be the deployed artefact.

Fast Start-up Times

This is similar to the previous point but becomes especially important in server-less applications where latency and cold starts are an important factor in a language/platform's suitability. Check out Benchmarking AWS Lambda runtimes in 2019) and look at the cold start performance of Python and NodeJS.

Python cold-start is more than 400x faster than Java

Generic Programming and Simpler Errors

Sometimes an algorithm can be expressed easily in code but shaping it to provide the correct information to a statically typed compiler can be difficult. Dynamic languages make generic programming trivial.

// Java Filter Implementation
public static <T> Iterable<T> filter(Iterable<T> items, Predicate<T> predicate) {
    ArrayList<T> result = new ArrayList<>();
    for (T item: items) {
        if (predicate.test(item)) {
           result.add(item);
        }
    }
    return result;
}
# Python Filter Implementation
def filter(items, predicate):
    result = []
    for item in items:
        if predicate(item):
            result.append(item)
    return result

Quite often in statically typed languages the error messages can also be very complicated. Especially when considering generic/template programming which a lot of standard libraries exploit.

Consuming Data Sources Requires No Additional Code

We don't need to create classes to read JSON, SQL, XML etc. We can simply read the data and start using it. Sometimes we still create classes or named tuples to make the code that uses these objects easier to work with, but it isn't necessary.

Generally Easier to Learn

All of the above can make dynamic languages better languages for learning to program with.

Static Languages Revisited

Modern static languages have many of the benefits that you would attribute to dynamic languages. So, let's revisit the benefits of dynamic and see if any fit.

Less boilerplate Code

If we look at languages like Kotlin, C#, TypeScript we see very succinct syntax, comparable to their dynamic counterparts. Type inference means that often we don't even need to add type annotations to benefit from static types.

# Python
def is_even(number):
    return number % 2 == 0
// Kotlin
fun is_even(number: Int) = number % 2 == 0

Since this is a small function, we can use an expression body which is more succinct than the whitespace laden Python and the return type is inferred.

If we compare JavaScript and TypeScript, quite often the code is identical, but the difference is that TypeScript will have all of the advantages of static typing above (except for performance improvements).

// JavaScript
const employees = await readGraduates();
employees.forEach(x => x.applyBonus(1000));
// TypeScript
const employees = await readGraduates(url);
employees.forEach(x => x.applyBonus(1000));

The code above makes use of the async / await syntax which is my preferred way of handling asynchronous programming. In JavaScript you can sometimes forget the await keyword on the promise calls. This is usually caught in TypeScript as above we can do a forEach on an Employee[] but not on a Promise<Employee[]>. We have the checking without any additional code.

This is potentially disingenuous as we have additional annotations in the method declarations, but TypeScript can infer function returns so again, if we look at readGraduates we see little difference in the code.

// JavaScript
function readGraduates(url) {
    // ... return a promise
}
// TypeScript
function readGraduates(url: string) {
    // ... return a promise
}

Fast Development Cycles

Historically I worked a lot with C++. I've worked in codebases where the build of a module could take ~10 mins and a full rebuild of the same module would take ~30 mins. I've seen applications where the build of all modules and dependent libraries can take close to 24 hours.

This is not the world we live in today with modern statically typed languages. Java, Kotlin, C#, TypeScript etc. have short compile times. We can easily have setups that watch our code files and as we save the code it is automatically compiled, hot reloaded or the application restarted. We can also have unit tests run on these saves as well.

Fast Start-up Times

Some frameworks (ahem, Spring) can give static languages a bad reputation in start-up time, but typically native and VM based languages will be faster than dynamic languages. We may see cold starts in server-less perform better for dynamic, interpreted languages, but we also see C#, F# have much better average duration (see Benchmarking AWS Lambda runtimes in 2019).

Also, something like TypeScript compiles/transpiles into JavaScript so we can use it with NodeJS for the exact same performance as raw JavaScript.

Generic Programming and Simpler Errors

I'll have to give it to dynamic languages like Python and JavaScript here. Especially considering some of the errors messages that I saw in C++. I'll caveat it slightly by saying you will still see errors in dynamic languages, but you will hit them at runtime. Runtime error cycles are slower than compile time ones. Let's hope you catch the runtime errors during development and testing and it is not your customer who finds them for you.

Easier to Consume Data

Again, I have to give this one to dynamic languages, but I'll talk more about this in Hybridisation below.

Easier to Learn

I think Python is a fantastic language to learn programming and I will give this one to dynamic languages as well. But I write code for a living. I'm past the initial learning stage (although this job is one in which you agree to do homework for the rest of your life), and this feature is less important to me. Day to day productivity, code maintainability, stability etc. are much more prominent in my mind.

Hybridisation

We have seen communities envy the greener grass of the opposite paradigm. Languages like JavaScript saw the introduction of Flow in 2014 to add static type checking to this dynamic language. Before this Microsoft had went further with TypeScript back in 2012. TypeScript is one of my favourite languages. As a strict superset of JavaScript, it brought just enough on top of JS to make development much more pleasurable. I think they have made excellent engineering decisions, recognising the importance of JavaScript, facilitating easy interop and leveraging existing JavaScript APIs. Other languages transpile to JavaScript (e.g. Kotlin, Scala, F# etc) but don't offer the same smoothness of development.

Even Python, another incredibly popular dynamic language introduced Type Hints in version 3.5 to have some of the static advantages listed above when programming in Python. It is much more cumbersome than static-first languages and limited in the assistance it can give but it can still help.

from typing import Callable, List, TypeVar

T = TypeVar("T")
U = TypeVar("U")

def filter(items: List[T], predicate: Callable[[T], bool]) -> List[T]:
    result: List[T] = []
    for item in items:
        if predicate(item):
            result.append(item)
    return result

Even if you don't use these features and work in JavaScript or Python, you are benefiting from them as the IDEs can leverage the type information others have written to provide better error checking and auto-complete in your dynamic code. Type bindings have been provided for the most popular APIs. But wouldn't you want the same thing for your own APIs too?

From the other side we see plugins and language features trying to add dynamic benefits to statically typed languages. For example, with dynamic languages it is much easier to simply consume a data source without having to write all the classes and boilerplate to make this happen. C# supports dynamic types. This allows data to simply be consumed and we'll find out at runtime if it works (fingers crossed everyone).

// This is C# but the employee here has no coded type
// As each line executes at runtime the method and property
//   will be checked.
dynamic employee = ReadCeo();
employee.ApplyBonus(employee.Salary * 0.1);

The performance is much slower than standard C# with all of the runtime checking but that is the overhead you have with any dynamic language. It allows you use dynamic typing when required and then get back to static typing in other places.

F# has Type Providers which allows you to consume external data sources (SQL, XML, JSON etc) and have the benefits of compile time type checking, auto-complete etc. by reading the data source (or a sample) and creating the types at compile time. So, we have the benefit of not having to write the types (just like dynamic) but with the benefits of static types.

Conclusion

I've tried to be objective here but I am probably biased with my programming background. But I teach all the languages I've talked about, and I can see the benefits of each.

At the end of the day, I'm fairly lazy and I want an easy life. When I use a language like Kotlin, I dislike having to return to Java, because I know there is a better way. When I use dynamic languages, to me at least, it feels like more work in the long run. I find myself seeing more runtime errors, and this is frustrating. In the small (e.g. server-less functions), these runtime error cycles can be more acceptable but as we write in the small we are often writing glue code, often using new, unfamiliar APIs and having assistance in the IDE can help productivity.

So whichever way you fall in your preferences of language or programming paradigm, don't be too militant and keep an open mind about the benefits of other techniques. Simply keep busy being lazy! Try to have your cake and eat it too.

There is no cake

Instil offers a range of training courses for the languages mentioned throughout this article. If you are interested, here are some useful links below:

Article By
blog author

Eamonn Boyle

Instructor, developer