Why The Fuck Did It Take Me This Long To Learn About clojure.spec?


Here's a hot take: you can't add a static type system to a language that was originally dynamically typed without it becoming a horrible mess. Without the backing of the language from the start, any type system added on will be more unsound, incomplete, and generally complex compared to the "real" statically typed languages.

I avoid gradual type systems like mypy for Python or Sorbet for Ruby because they ruin the prototyping speed of these nice, simple, dynamic languages to add a shitty type system that only attempts to "fix" one specific problem of said dynamically typed languages and doesn't even "fix" it all that well.(1)

(1) Pro tip: if you're having issues with a language because it doesn't have static types, you can either change to a language that does or ideally, you can WRITE BETTER CODE. This isn't a joke, this is legitimate advice on how to solve your issue. Your welcome. Idiot.

This even extends (but not as much) to supersets of dynamic languages that add a proper type system like TypeScript. Yes, it's nice that it has types from the ground up, but at the end of the day, it's just JavaScript… but with static types! WOW! There's only so much that adding static types can do for a language that treated types as a suggestion in the first place.

The designers of TS also had to consider the skill level of your average JS developer when designing the type system. JS developers do not understand type theory. They don't know what soundness and completeness means. They don't understand why certain things are covariant, invariant, contravariant, or bivariant.

I'm not saying they should as knowledgeable in type systems as Haskell developers but sometimes it feels like JS developers don't even have object permanence yet when you consider the amount of fundamental programming language design knowledge they have (or lack thereof). And you know what other category of human doesn't have object permanence yet? Babies.

I'm confirming it right now: JS developers are babies. You know it's true because this post is on the Internet. And the Internet never lies.

So the TS designers had to make the type system as easy as possible to understand so that all our two month old JS developers could use it while sipping on their baby bottles, playing on their iPads, and writing dark patterns for a mega corporation's website.(2)

(2) Babies have the minimum amount brain function and capacity required to write JS code but have not yet developed any morals or ethics, thus they are the best kind of developer for any mega corporation.

Now, what is the result of this "easy to understand" type system?




So anyway, for the past while I've resigned myself to my favourite dynamic language, Clojure, not having static types ever, because even with the meta programming power of being a Lisp I assumed it will still probably be a bit shit. And if I want types I might as go all out on not having any prototyping speed and just use Rust.(3)

(3) Before you say something like "Go's prototyping speed is actually quite good because of the simplicity of the language." I would like you to please consider this counterargument: Go sucks.

Enter Clojure spec, and let me tell you, learning about this Clojure library has never made me so angry and aroused at the same time before. I've heard about spec before, but never really bothered to actually look into it until now. This was the worst mistake of my life,(4) I can't believe I've been writing "spec-less" Clojure code this entire time like some sort of fucking idiot who thinks 80 MB (half of which are from tracking scripts) is an acceptable size for a website.

(4) And I've made many mistakes in my life. For example: learning JavaScript.

Spec is sort of a response to many people's main (although fair) gripe that Clojure doesn't have a static type system. But it's a response that they may not expected as it doesn't take the traditional approach of checking types statically. At a very fundamental level spec is a declarative language that describes data, their type, their shape. Spec follows the general philosophy of Clojure in that all of its functionality is available at runtime, you can use it, introspect it, generate it – there is no extra step before execution when the compiler checks your whole codebase for errors.

To be honest, a static type system and specs aren't actually all that comparable (I'm going to keep comparing them though). Specs can be considered a trade-off between static and dynamic typing, but I think it's an extremely compelling trade-off. Two of the big advantages of typed languages are communication (documentation) and robustness. These are gained back with spec and other good libraries like malli.

What you get with spec goes way beyond what a strict, static type systems gets you, such as arbitrary predicate validation, freely composable schemas, automated instrumentation and property testing. You simply do not have these in a static world. It speaks to the power of Clojure (and Lisp in general) that these are just libraries, not external tools or compiler extensions.

One of the great thing about specs is how they are added à la carte and only applied to the things you specify, while static types have to be specific everywhere. I know you can technically do this in gradual type systems, but the point of a gradual type system is to gradually add types to an existing un-typed codebase. This will inevitably cause some types to be an implicit any when you are trying to mix typed and un-typed code together, making it generally unsound.

This is great for prototyping because you can write and iterate over the implementation in your usual Clojure style and when it's done, write the spec for correctness, validation, testing etc. Of course, you can write the spec first as well and plan it out like that, but the point is that it's flexible and up to you.

I also find that you still need the same tests using a statically typed language that you would need using Clojure specs. The reason being that the type system does little to ensure semantic correctness.

Spec, on the other hand, validates and conforms any predicate (not just type) for the args, the return, and can also validate relationships between the two. All of this is external to the function's code, separating the logic of the function from being commingled with validation and documentation about the code.

For example, consider a sort function. The types can tell me that I passed in a collection of a particular type and I got a collection of the same type back. That's great, I know the types of things in the collection are correct, but did the collection actually get fucking sorted? This is what you really care about at the end of the day, does the function do what you intended.

This is difficult to express using most type systems out there, if not outright impossible. If we were to look at the language with the best type system, you could use dependent types to express it,(5) but it certainly wouldn't be something that you get for free with Haskell and certainly not in TypeScript. So you'll still have to write tests to ensure that the function is semantically correct for anything non-trivial.

(5) Haskell's dependent types aren't the best example of depedant types, but I know literally nothing about the languages that actually have "correct" depedant types.

None of the ideas in Clojure specs are new. In fact, I'm reminded of a language from the 80's that did something similar called Eiffel, with its "design by contracts and assertions" which spawned this entire "Design by Contract" approach to software.

I'm also not going to say the specs are the end of programming language design and that static types are officially CANCELLED, because I'm too much of a Rust fanboy I still think static types are good for certain things.(6)

(6) I also never said static types were bad.

Static typing allows you to write monolithic projects that have lots of internal interdependencies. Refactoring often becomes painful when a particular piece of data is used in many parts of a monolithic project. When you change the shape of that data, then you have to make sure you update every place that uses it. This is where static typing can help ensure that you didn't miss anything in your refactoring and isn't really a spot that specs hit.

Right, so now I've just noticed that I haven't given any examples and that I probably should do that instead of rambling on for 1500+ words. But I'm not, because I'm lazy I want you to develop as a person and look for things yourself.

Good luck and goodbye.

Ok fine… here's a talk by Stuart Halloway that just says everything I said but better (there are also examples part way through). You should have just watched that instead of reading this shit in all honesty. Idiot.