Sunday, July 1, 2018

Cast Off the Training Wheels

Modern programming languages have too many training wheels.  In this context, training wheels are language "features" that arbitrarily restrict what the programmer can do with that language.  In Java, this includes things like forbidding multiple inheritance and requiring functions to either handle or declare that they can throw errors.  In Rust, this includes pointer ownership.  Most high level languages deliberately forgo any mechanic for direct memory allocation and manipulation.  All of these are "training wheel" features, designed to prevent the programmer from getting into trouble, but they also all take decision making power away from the programmer, and most deny the programmer some very valuable tools.  Humans have been programming for well over a century, and we have been programming physically existing machines for over half a century.  Isn't it time to take off the training wheels?

Like early bicycles, early programming languages did not have training wheels.  Restrictions were typically the result of practical considerations.  For example, dynamic typing requires a far more complex compiler, and it requires the program to store types and do frequent type checking.  Hard coding types make code faster and easier to compile, and it produces much faster programs.  On systems or in applications where performance is important, static typing is the practical choice.  As computing systems grew in resources, development time grew in priority, and languages with dynamic typing were invented.  Then people started arguing about the meta, and the concept of type safety was born.  Something similar happened with multiple inheritance.  Object oriented programming was invented to provide a thought model that is easier for the typical human to understand.  Inheritance was invented to allow this model to support more abstraction.  Multiple inheritance was added to allow for even more abstraction.  High levels of abstraction are associated with faster programming (though it neutralizes the benefit of making programming easier for regular people to understand).  Then someone pointed out that multiple inheritance can result in ambiguous code, and Java decided it need to solve this by eliminating multiple inheritance and replacing it with a similar, far more limited mechanic, that eliminates the ambiguity possible in multiple inheritance at the cost of a significant chunk of the benefits of multiple inheritance.  This was not a decision born from practicality.  It was deliberate training wheels, designed to protect programmers from themselves, at the cost of some of their freedom.  Rust's ownership system for dynamically allocated memory is another example, where someone noticed some common mistakes programmers make and decided to provide training wheels to make that mistake impossible, again at the cost of eliminating some legitimate and valuable options.  It was an ingenious solution, but it comes at the cost of making the language less powerful and the highly skilled programmer less productive.  Training wheels are great when they are needed, but to the skilled professional, they often prevent optimum performance.  On a bike, training wheels limit the maximum safe speed a cyclist can make a sharp turn.  In programming, training wheels lock up some very valuable tools.  Now that we have training wheels, we need to learn when it is time to cast them away.

Instead of training wheels, many modern vehicles have warning systems.  If you are following too close or are about to hit someone while backing up, an alarm will sound.  The car will not prevent you from continuing your action, but it will let you know that what you are doing is not safe.  Vehicles that have attempted to second guess the driver have been known to cause accidents or make accidents worse.  Sometimes, briefly following too close is necessary to avoid an accident.  Sometimes backing into a parked car is better than getting hit by someone driving recklessly.  And the driver should always be able to correct any mistake the car might make.  In addition, imagine a car that stops at every crosswalk and train crossing, when you can clearly see that no one is coming.  Some of these "features" are more like a car that simply won't ever drive across a crosswalk or train crossing, whether it is safe or not.  Safety systems are not a bad idea, but machines are still not even close to being able to judge complex situations as well as humans can.  Warning systems that allow for human judgment are currently far superior to systems that just refuse to do certain things in the name of safety.

The same applies to programming languages.  The language should not try to second guess the programmer.  That variable that is assigned a value and then never used might exist to add a critical delay during the initialization of a microcontroller.  Warning the programmer that the variable is not being used is useful, but refusing to compile until the variable is used is counterproductive.  Warning systems do not have to be built into languages either.  In fact, in general, they are better as separate programs, where possible.  The C compiler does not need to warn the user about style errors.  In fact, it is far more convenient when concerns are separated.  Linters warn about style errors, and compilers should stick to syntactical errors that prevent compilation entirely.

The fact is, compilers should not care about best practices, because in software engineering, best practices are heavily dependent on application.  Linters are where we are already doing most of the verification of best practices, and they make a good place to verify even more.  Linters cannot prevent compilation.  The best they can do is issue warnings.  But warnings are enough when it comes to best practices in software engineering.  Some warnings can and should be ignored.  Sometimes the best practice for a particular problem is not the general best practice.  Instead of enforcing best practices in language definitions, we should be using external tools to verify best practices and giving programmers the full set of available tools in our programming languages.  The best practice in engineering does not include taking away tools that are easy to misuse in ways that are harmful (otherwise, engineers wouldn't have any tools).  The best practice is to provide all available tools but include safety protocols that will prevent harm when they are properly followed.  In programming that means making languages that allow anything and protocols and linters that will warn users when they are doing things that are potentially harmful.

It's time to take off the training wheels, so programmers can do their jobs better and more efficiently.  It's time to take best practices out of our programming languages and put them into linters, so that we will finally have access to full, rich array of programming tools in addition to the ability to use them safely.

No comments:

Post a Comment