Our beloved Uncle Bob has recently published two posts on his Clean Code Blog relating to my previous post:
In these posts, he poses several questions:
- Should we try to close every loophole in a language by adding more constraints?
- Who should manage risk related to NPE’s, exceptions, inheritance, etc.? The language or the programmer?
- Can tests be used to reduce the risk instead?
As expected, he has strong opinions about the answers to those questions. Alas, those answers are different from mine. Let us go over a few of them.
Should we try to close every loophole in a language?
Everyone that has read my first post knows that I am a big fan of closing loopholes through the type system, for example with optional types to eradicate null pointer exceptions. Uncle Bob has two main problems with this approach:
- It makes the language more complex by adding more keywords
- It requires a lot of up-front design
It makes the language more complex
Concerning the first point, he is factually correct in that the language becomes more complex. I am also a firm believer in the power of simplicity. Every piece of added complexity must be warranted. However, the ways I see it, relatively safe languages such as C# or Swift are still not nearly as complex as – say – C++; a language that is also much less safe. Many software developers have been able to master C++, so I presume even more developers will have the cognitive capacity required to learn C# (including optional types) or Swift.
On a related note, programming languages are languages in the same sense that natural languages such as English or French are languages. They each have a certain syntax and grammatical rules. Natural languages are however far more complex than programming languages. The number of keywords in a typical programming language is something in the order of tens or maybe hundreds. It is not as easy to come up with a single number of unique words for a specific natural language, but it will always be at least several orders of magnitude larger. Since almost all humans are capable of learning at least one natural language, I think it is safe to say that developers – generally a pretty clever folk – can handle a couple more keywords in their programming languages.
On top of that, once you have mastered a safer language, even if it takes a while longer, the gains are permanent. In other words, adding constraints to code will require fewer and fewer mental resources the more experience you have with them, while in turn rewarding you by requiring even less mental resources to interpret the code.
It requires a lot of up-front design
Uncle Bob’s second issue is that the constraints all have to be thought of up-front, which hinders the agile way of working. I do not experience this problem in my job as software engineer.
When I design a system from scratch, even if I do not have the full specifications handy, I will simply make some basic assumptions. For example, when I design a class with a couple of properties, I will assume that all of them are required. In my experience this is a good heuristic, and I rarely have to completely overhaul my classes afterwards.
Of course once in a while, I realise that a particular fields needs to be optional after all. In a language where the “optional loophole” has been closed, many method signatures along the calling chain (or tree) will potentially have to be updated to make the code compile once more. I do not experience this as negative because of two reasons. One, developers should be refactoring often anyway. Code is in my experience rarely good the first time you write it. It needs to be kneaded and rekneaded like dough until something acceptable comes out. This should be part of your daily routine and not an objection to not do something. Two, I do not have to personally go look for all the places where I used the field, the compiler will tell me instead. Most of the time, the data is just passed along or transformed in some trivial fashion. These two processes are very trivial to “maybe-fy”. (So trivial in fact that someone could one day write a refactor tool for it.) At the end of the chain where the data is actually used, I now have to decide how I will deal with the missing value. This is the part where I want to invest my cognitive capacities in, not in manually searching for the places along the chain.
Contrast this with a language that does not discriminate between normal and optional types. Nothing in the code has to change per se when the specification of some field changes from required to optional. After all, every field is implicitly optional in such a language. However, unless perhaps if the developer of the project suffered from some extreme form of OCD, he will not have checked whether the value was available at every point along the chain. This means that although the code compiles, something somewhere will break when the field is suddenly left empty. Now the developer has to invest his cognitive resources in finding those places instead of using them for more useful tasks. Since these resources are scarce (they diminish over the course of the day and recharge during sleep), it is a shame to waste them on work that ought to be trivial.
This line of thought can be generalised to any form of constraint, not just optional types. Do not be afraid to get your hands dirty and refactor your code, and let the compiler – which is aware of the constraints – help you in locating and fixing violations. This way the up-front design can still be kept to a minimum.
It is true that regardless of how I feel, some people will perceive excessive refactoring as punishment. They will indeed – as Uncle Bob stated – simply override all safeties. However, this behaviour can already be observed in a relatively loose language like C#. It is certainly not unique to strict languages such as Swift or Haskell. For example, a while ago I noticed a colleague of mine marking all methods as virtual in a new internal framework, “just in case”. Some background: in Java every method is virtual unless explicitly annotated with
final, whereas in C# the keyword
virtual is necessary to enable it. The chief C# architect had his reasons for this decision, particularly to decrease risk caused by unanticipated method overrides. In the end, we were able to convince our “just in case” heretical colleague to undo his changes. At the moment I did not think of the Chernobyl example, but it certainly would have made our case even more compelling.
Who should manage the risk?
Now, ask yourself why these defects happen too often. If your answer is that our languages don’t prevent them, then I strongly suggest that you quit your job and never think about being a programmer again; because defects are never the fault of our languages. Defects are the fault of programmers. It is programmers who create defects – not languages. ~ Uncle Bob
This is mostly a philosophical discussion. However, his arguments remind me of those used by Americans when discussing gun control: “guns do not kill people, people with guns kill people”. As a European, I feel that simply banning guns seems like a much more cost-effective and robust solution compared to teaching all people how to properly handle guns. People – even those with the best intentions – occasionally fail to uphold certain standards, rarely but still too often with dramatic consequences. Proper gun control (and compilers) may fail too sometimes, but certainly at a much lower rate. This concludes my political intermezzo, my apologies.
Software development is hard enough as it is, and I think we should accept all the help we can get to increase our productivity and reduce risk. That way we can use our limited cognitive resources for creating things that have actual business value.
Can tests be used to reduce the risk instead?
If Uncle Bob were to read the above paragraphs, he would say he does not need a compiler to show him where the errors are, he has tests for that. I am sure his test coverage is admirable, but in reality tests are often an afterthought. I am not saying tests are not important – to the contrary, but I am realistic enough to know that looming deadlines often cause the “less important” parts of our engineering discipline such as testing to be pushed further and further down the priority list. Yes, something should be done about that, but that is not going to save us today.
What can perhaps save us, is techniques that have to be integrated with the actual code you write. Techniques such as optional types! After all, you cannot possibly skip writing the actual code. Integrating some extra constraints is a lot more likely to get done than writing a complete test suite.
Some people responding to Uncle Bob’s first blog post above argue that types are in essence tests. In the second blog post Uncle Bob (extensively) disagrees with this point of view. In my view types are not tests either, they are even better! Say you have a statement such as . From a mathematical point of view, there are two ways to proceed. One, you think the statement is correct and you try to prove it formally. Two, you think the statement is incorrect and you try to find at least one counterexample. How is this related to types and testing you ask? Our code can also be seen as statements, and their correctness can either be proven or contradicted. Type safety corresponds to the former; the compiler generates a proof that a method call or variable assignment makes sense according to the rules of the type system. The power of this system is very underappreciated in my opinion! Testing on the other hand is related to (but not equivalent to) the latter. After all, we assume that our code is correct but testing at best enables us to perhaps show that it is incorrect. It is fundamentally unable to prove the correctness, no matter how many tests we write. Dijkstra once said it very succinctly: “testing shows the presence, not the absence of bugs”.
Uncle Bob argues that proofs by types are cool and all, but that they only prove the internal consistency of the system, not the external behaviour that generates business value. He has a point there. However, another technique I like, Domain Driven Design (DDD), suggests aligning your code with the business domain as much as possible. When this suggestion is followed, internal consistency very closely resembles external behaviour. It will never be a perfect match of course, but I see it as a very promising way of working nonetheless.
P.S.: My apologies for using the word “cognitive” so often in this post. I recently attended a course in cognitive psychology, and I am determined to get my money’s worth!