Anybody who’s worked with me for a significant length of time knows that I’m a fan of an idea I call Compiler Driven Development. They know that I like compiler errors and encourage developers to start with code that may not compile and use the compiler to iteratively make better sense of it.
What is Compiler Driven Development?
Consider the case of a dictionary keyed off an object’s identifying property. Let’s assume that property is an integer. So we have a dictionary keyed off of an integer. The problem is any integer will do yet we intend to only use the identifying properties of our original objects. To use any other integer would obviously be an error. In this theoretical code’s current form this error wouldn’t be spotted until the code is running. Instead of time spent testing looking for interesting bugs, instead it’s spent discovering this.
Compiler Driven Development can be applied to this problem. We can change the type signature of the dictionary to only accept our original object. We can move the concern of figuring out the identifying properties of those objects into how we obtain their hash code and their notion of equality. Now it’s not possible to use the identifying property of a different object to key off of this dictionary. Testing effort doesn’t need to be spent on this (now nonexistent) issue and moves on to questioning more interesting aspects, such as performance and usability.
This situation can be expanded upon. What would happen if this dictionary was used all over the application. By refactoring the signature of the originating dictionary the compiler will tell you the exact location of the keying assumptions that were made. It becomes a trivial exercise to find and correct them. Without the compiler’s help the regression testing scope would be massive and this refactor may not be feasible at all.
Compiler errors, when done right, communicate that what was written doesn’t make much sense. Without the build breaking these would be errors leaking out to production. These errors might be immediately obvious or could be in some neglected backwater of the application where they’ll resurface a year after the originating changes were made. A compiler can check that certain categories of errors of the categories are not present in a large application and it can do it much faster than any human being could ever hope to.
There are ways around compilation errors where one can basically opt out of the warm protective cocoon of the compiler’s automatic error checking. Thankfully these methods are very obvious, such as explicit casting or using strings instead of a more indicative type. Whenever I see these, the first thing I think is, “here be dragons” because the compiler no longer has my back.
Staticly Typed Languages only.
This means over time unit test suites will shrink in size for statically typed languages. This is a good thing! It means less code that must be maintained with the same amount of safety. It also means adding features requires less code to be written for the same amount of safety. Both for maintenance and new features benefit from these language features in statically typed languages.
Static typing means I can guide other developers in how code is meant to be used. StackExchange.Redis has a wonderful little class, called RedisValue, which is a good example of this. RedisValue implicitly converts from a number of types. Simply scanning the class definition I am immediately aware that byte arrays, strings, ints, doubles, etc are all easily supported. If I try to cram an instance of my Foo class into Redis, I’ll quickly find out there’s no good conversion readily available and I’ll need to figure out how to map into one of the available conversions. I find this out at build time, before I’ve run any code at all. In fact, likely I’ve found that out before I even went to build due to type checking done in the IDE.
One tool in a box of many.
Compiler Driven Development doesn’t replace Test Driven Development, just like how Behavior Driven Development doesn’t replace Test Driven Development. Both Compiler Driven Development and Behavior Driven Development improve upon Test Driven Development but do not replace it.
Compiler Driven Development is more about good tooling than anything else. It’s about leveraging the work of the very intelligent people who have come before us and avoiding having to encounter the same simple regressions which they have proven avoidable. It’s about having the freedom to make a tiny change in one location and seeing it ripple out before the users in production do. It’s about communicating intentions behind the code in such a way that they are enforced long after you’ve moved on.
There are some things Compiler Driven Development cannot catch. It cannot catch usability issues and performance issues. It cannot catch issues which cannot be caught by the compiler (aka, they cannot be encoded into the type system). It cannot catch erroneous implementations of algorithms. I seriously doubt it will replace good testing practices in my lifetime, if ever. It can however make those testing practices much more effective by reducing the scope to more interesting problems.
Compiler Driven Development is not, “it compiles so it must be right”. It’s, “it compiles so I know that these properties hold”. As staticly typed languages become more sophisticated we will be able to encode more and more of these provable properties. Knowing how to leverage the type system becomes more and more important as the weight of code increases.
I do not envy those writing code for large Node.js applications.
P.S. Linting counts as a light form of Compiler Driven Development. Linting still allows programs which may not work to be run in spite of the linting tool identifying the issue.