Blog Home RSS Kelvin Jackson

Clojure and Type Systems

2022-02-07

So I've been writing a lot of Clojure lately. Although I had a bit of Clojure experience before this project, and a lot more with Scheme and Racket, this was my first time using Clojure in a professional setting, and my first time in a long while using a language in the Lisp family for a truly substantial project.

For the uninitiated, Clojure takes "being lax with the type system" to a whole new level. While it's certainly possible to get a type error out of Clojure (and I daresay easy if you're used to JavaScript, since the easiest way is to try to concatenate strings with the + operator), there are a lot of circumstances where JavaScript or Python would throw an error, but Clojure will just evaluate through. Look up a key that isn't present in a hashmap? You just get nil. Look up a key in an identifier you thought was bound to a hashmap, but is actually a primitive or nil? Also nil! Same thing if you don't provide an else case in a conditional expression, or don't return something from a function, etc. In this way, Clojure is arguably "safer" than JavaScript, because while it will sometimes just evaluate right through when other languages would throw a useful error, you usually just get nothing, rather than the garbage that JavaScript can produce.

But this isn't what I'm here to talk about today. Today, I'm here to talk about how the lack of static type system and the laxity of Clojure's dynamic type and nil checking have affected my ability to write and debug code.

In a sentence... it really hasn't.

I thought the laissez faire approach to types was going to be extremely annoying when I first started working with Clojure. But now that I've been at it for a while, it really isn't. While I've made plenty of mistakes that would have been caught by static (and many dynamic) type checkers, have I truly lost as much time to them as I would have lost to writing boilerplate code in Java? Especially boilerplate code needed only to test a hypothesis while debugging that you have to write because the type system will only accept a certain class? And what about all the other bugs that crop up? I'm far from %100 sure, but I'm very skeptical that stricter type checking would make my life easier here — and it just might make it harder.

Furthermore, it's easy enough to validate data when you need to. In Clojure, this is usually accomplished using schemas, but you could easily write a domain-specific checker for a particular dataset if you needed it. I do validate data in Clojure when I need to, but it doesn't need to happen at every function or operator call and every identifier binding — and why create a class when a map will do the trick, without tying you to custom API? (This is a goal of Clojure.)

So, with all of that said and done: I still like static type systems. They have their place, and sometimes it helps to have certain mistakes caught at compile time. However, you can still write good code without them, and you don't necessarily lose much more time to type issues than you would in any other language. If you write decent tests, you'll catch those bugs either way.

(You're writing tests, right? ...right?)

Just make sure you validate user input and maintain decent test coverage, and you'll be fine, whether you're using a statically typed language or not.