I generally prefer statically typed languages. I just do. All else being equal, I will choose a statically typed language over a dynamically typed language every time. But why? And why did I qualify that statement?
Well, that’s a little more complicated.
Let’s take a look at a simple function that determines whether or not a value is prime. The version below, written in JavaScript, doesn’t technically contain any type information.
function isPrime(n) {
let i = 2;
while (i <= Math.sqrt(n)) {
if (n % i === 0) {
return false;
}
i++;
}
return true;
}
When I say that this code doesn’t “technically” contain any types I mean that there are no types specified in the program text. The parameter n
, the local variable i
, and the return value could be anything, at least as far as JavaScript is concerned. There are, however, some implied types. For instance, the value we provide for n
must be in the domain of the %
(modulo) operator.
But, as programmers, we care about more than the language-level semantics of our programs.
The idea of a prime number itself only really makes sense for integers greater than 1. It wouldn’t make sense to ask whether the string “hello” is prime, for example. So while JavaScript doesn’t really care about types, we certainly do, because our problem domain (primality) does.
If we rewrite this function in Dart, a language that allows type annotations, we can capture at least some of the semantics of our problem domain within the text of our program.
bool isPrime(int n) {
int i = 2;
while (i <= sqrt(n)) {
if (n % i == 0) {
return false;
}
i++;
}
return true;
}
Note that it is no longer possible to pass “hello” to this function (well, you can, but the program won’t run). This is helpful because inputs other than integers don’t make sense within the problem domain anyway. So rather than add code to handle such mistakes, we can change the program so that it will refuse to even compile / run.
The point here is that our problem (finding prime numbers) has types, so it makes sense for our program to have (the same) types.
However, you might have noticed that our second function doesn’t actually have “the same” types as our problem. Ideally, we would like to require that n
be an integer greater than 1. Unfortunately, we can’t express this idea with Dart, at least not in a way that would be likely to result in a satisfying experience for users of our function.
While the types don’t get us quite to where we want to be, we can still use runtime checks to finish the job. In this case we can provide some pretty helpful error messages as well. We could also decide to just return false
for integers that don’t make sense (this is also a nice example of how we can define errors away). That being said, something is better than nothing, at least in my opinion.
bool isPrime(int n) {
if (n < 2) {
return false;
}
int i = 2;
while (i <= sqrt(n)) {
if (n % i == 0) {
return false;
}
i++;
}
return true;
}
Sometimes, the problem domain itself is more difficult to translate into a program and types can help smooth the way. For example, say we have a function that accepts a URL:
function sendRequest(url) {
// ...
}
What does a URL look like? Do we need the leading “https://”, or is this a situation where we can use “//” to infer the protocol (like the href
attribute on an HTML anchor tag)? Furthermore, how do we verify that what we were handed is a valid URL? That could be a lot of work. We could write a reusable function to validate a URL, but if we’re going to go down that road we might as well make it a type.
void sendRequest(Uri url) {
// ...
}
Once again, our problem domain has types, and by introducing those types into our program we can both simplify our code and make it easier for others to use.
Earlier, I implied that I might choose a dynamically typed language under certain circumstances. A programming language is just a tool, an abstraction over the machine to facilitate human interaction. The right tool for a job depends on the job.
Writing a browser extension, for example, is easily done in JavaScript (although today TypeScript is closing the gap). Elixir / Erlang can be a great choice for scalable server applications. Racket makes it easy to create DSLs. There is a seemingly infinite selection of machine learning and data analysis tools based on Python. In these cases, and others, the ecosystem that surrounds a language can be important enough that it outweighs other considerations, such as static types.
At the end of the day, I prefer statically typed languages because they allow me to represent more of my problem domain in the program itself. But I try hard to remember that the right tool for the job isn’t the one I like best, but the one that is most likely to result in a correct, useful piece of software.