1. Inability to reason about code
Reasoning about code means being able to follow the execution path (“running the program in your head”) while knowing what the goal of the code is.
- The presence of “voodoo code”, or code that has no effect on the goal of the program but is diligently maintained anyway (such as initializing variables that are never used, calling functions that are irrelevant to the goal, producing output that is not used, etc.)
- Executing idempotent functions multiple times (eg: calling the save() function multiple times “just to be sure”)
- Fixing bugs by writing code that overwrites the result of the faulty code
- “Yo-Yo code” that converts a value into a different representation, then converts it back to where it started (eg: converting a decimal into a string and then back into a decimal, or padding a string and then trimming it)
- “Bulldozer code” that gives the appearance of refactoring by breaking out chunks into subroutines, but that are impossible to reuse in another context (very high cohesion)
To get over this deficiency a programmer can practice by using the IDE’s own debugger as an aide, if it has the ability to step through the code one line at a time. In Visual Studio, for example, this means setting a breakpoint at the beginning of the problem area and stepping through with the ‘F11′ key, inspecting the value of variables–before and after they change–until you understand what the code is doing. If the target environment doesn’t have such a feature, then do your practice-work in one that does.
The goal is to reach a point where you no longer need the debugger to be able to follow the flow of code in your head, and where you are patient enough to think about what the code is doing to the state of the program. The reward is the ability to identify redundant and unnecessary code, as well as how to find bugs in existing code without having to re-implement the whole routine from scratch.
Object Oriented Programming is an example of a language model, as is Functional or Declarative programming. They’re each significantly different from procedural or imperative programming, just as procedural programming is significantly different from assembly or GOTO-based programming. Then there are languages which follow a major programming model (such as OOP) but introduce their own improvements such as list comprehensions, generics, duck-typing, etc.
- Using whatever syntax is necessary to break out of the model, then writing the remainder of the program in their familiar language’s style
- (OOP) Attempting to call non-static functions or variables in uninstantiated classes, and having difficulty understanding why it won’t compile
- (OOP) Writing lots of “xxxxxManager” classes that contain all of the methods for manipulating the fields of objects that have little or no methods of their own
- (Relational) Treating a relational database as an object store and performing all joins and relation enforcement in client code
- (Functional) Creating multiple versions of the same algorithm to handle different types or operators, rather than passing high-level functions to a generic implementation
- (Functional) Manually caching the results of a deterministic function on platforms that do it automatically (such as SQL and Haskell)
- Using cut-n-paste code from someone else’s program to deal with I/O and Monads
- (Declarative) Setting individual values in imperative code rather than using data-binding
If your skills deficiency is a product of ineffective teaching or studying, then an alternative teacher is the compiler itself. There is no more effective way of learning a new programming model than starting a new project and committing yourself to use whatever the new constructs are, intelligently or not. You also need to practice explaining the model’s features in crude terms of whatever you are familiar with, then recursively building on your new vocabulary until you understand the subtleties as well. For example:
Phase 1: “OOP is just records with methods”
Phase 2: “OOP methods are just functions running in a mini-program with its own global variables”
Phase 3: “The global variables are called fields, some of which are private and invisible from outside the mini-program”
Phase 4: “The idea of having private and public elements is to hide implementation details and expose a clean interface, and this is called Encapsulation”
Phase 5: “Encapsulation means my business logic doesn’t need to be polluted with implementation details”
Phase 5 looks the same for all languages, since they are all really trying to get the programmer to the point where he can express the intent of the program without burying it in the specifics of how. Take functional programming as another example:
Phase 1: “Functional programming is just doing everything by chaining deterministic functions together”
Phase 2: “When the functions are deterministic, they don’t need to be executed until the output is called for, and only for as much as needed. This is called Lazy Evaluation and Partial Evaluation”
Phase 3: “In order to support Lazy and Partial Evaluation, the compiler requires that I write functions in terms of how to transform a single parameter, sometimes into another function. This is called Currying”
Phase 4: “When all functions are curried, the compiler can choose the best execution plan”
Phase 5: “By letting the compiler figure out the mundane details, I can write programs by describing what I want, rather than how to give it to me”
Read the full article here