On abstraction level dynamics in the process of programming and the productivity consequences
When writing code, one’s thought and desicion making process can be on different levels of abstraction. A rather simplified list of these levels (from highest to lowest) may look like:
- product-level requirements
- responsibilities and relationships between separate modules (architecture level)
- module considered in isolation
- function
- code statement / expression
- syntax tokens
- code text modification
When building software, one is constantly thinking and making decisions on certain levels, and switching between them. The consequences are that after working on a specific level, one must switch either up of down to observe the implications of the work from another level, and to alter/create the path to an optimal solution. Too much thinking on a certain level may deprive one of the ability to switch to a higher level, thus resulting in a more narrow view of the chosen solution.
The dynamics
A typical abstraction path while working on a product feature may look something like this:

Starting from the top, first the notion of the feature high-level requirements is acquired, then either a new module is created or an existing one is chosen to be modified, then the new functionality is added to module, and all the way to the bottom is typing code statements/expressions using the valid syntax.
In this case, a large amount of energy can be wasted on thinking and making decisions on the lower levels. However familiar the syntax of the language may be, the process of writing it is prone to errors and thinking about it is not as intuitive as, for example, thinking about module responsibilities and relationships. It is obvious that manually inspecting code for syntax errors and in-head evaluation for a large amount of time is highly counterproductive and energy consuming. The situation may be even worse in case of present mutable global variables, poor encapsulation, functions/modules with a large amount of responsibilities, etc. Another example is constantly switching focus between the thing you want to type and finding the right symbol on the keyboard — this process also wastes a lot of focus and mental energy.
Also, being on a lower level of abstraction could lead to one taking a different route from a higher perspective — investing a large amount of energy on writing code on a lower level for a reasonable amount of time induces weakens the ability to shift and view the project from a higher level perspective, leading to a poorer ability to make the right choices.
For example, an hour wasted on debugging a function with a body of 200-ish lines of code (lower level) may be the consequence of a wrong decision about the responsibility of this function (higher level), but can be unseen because the focus had been shifted to debugging and in-head evaluation for too long.
Another case may look something like the following:

Here one doesn’t loose himself in the lower levels for too long to loose focus shifting flexibility — this allows for easier assessment of the progress on product-level tasks and observation of the influence of the decisions made earlier, while taking the high-level tasks such as module responsibilities and project architecture into account.
An abstraction shift example
For an example of feeling an abstraction shift, we might consider writing a program that must:
- fetch a users list from the server
- sort the list by the amount of friends in descending order
- take the average of the amount of posts of the top 10 users from the list.
When taking the imperative programming approach with mutable data and callbacks for solving the task, the code (JavaScript used) may look something like this:
$.ajax({
method: 'get',
url: '/api/v1/users'
success: handleSuccess
});
function handleSuccess(response) {
var result = response.users.sort( (a, b) => (b.friendsCount - a.friendsCount) );
result.splice(10); for(var i = 0; i < result.length; i++){
result[i] = result[i].postsCount;
}var sum = 0;
for(var i = 0; i < result.length; i++){
sum = sum + result[i];
}
console.log(`The result is ${ sum / result.length }`);
}The problems of this code are rather obvious: the need to track mutable data changes, the counter-intuitive for-loops operations and overall poor readability. In order to write and read this code, one needs to put much energy into thinking on lower levels — the for-loop syntax, mutable “result” variable, the sorting function (a more general and in this case less useful abstraction than sorting by key). The consequence of needing to be on a non-intuitive abstraction level is the difficulty of modifying behavior, finding bugs, figuring out what the piece of code does by reading it.
If the functional approach with immutable data and promises is used for this problem, the result may look something like this:
fetch('/api/v1/users', {method: 'get'})
.then( ({users}) => compose(
result => console.log(`The result is ${ result }`),
average,
map(prop(‘postsCount’)),
takeLast(10),
sortBy(prop(‘friendsCount’))
)(users));Here, the abstraction dynamics in the process of writing and reading the code are much different — more time is spent higher and less lower. The key reason for this is the mapping between the intuitional approach to the task and the coding constructs used. The natural way to solve the given taks is by a composition of transformations of data, not the for-loops or mutating variables. The code reflects just that — the composition of :
- sorting users by friendsCount property
- taking 10 items from the end of list
- mapping the list elements to their postsCount prop
- taking the average of the post counts in the list
- outputting the result
This mapping between our understanding of the solution plan for the given task and the functional approach leads to more time spent thinking on a higher abstraction levels and an easier manipulation and increased readability of the code, thus giving more energy for assessing the code from a higher perspective.
Redux + React
The same mapping of the intuitional approach to the problem to constructs used in coding can be found in Redux+React solution — it helps building more stable, modifiable and extensible single-page-applications.
As with the previous example, the main reason why Redux+React gives great opportunities in coding complex asynchronous interfaces is that it is natural for humans to reason about SPA’s in terms of an extracted state value, views which map directly to the state value at any point in time, actions which are triggered either externally or by user input, and composable reducers - pure functions which take the triggered action with the current state, and return a new state.
Suppose we want to add a feature to an existing application, which enables users to leave a comment to an article. With React+Redux the process of adding it may be the following:
- Make a stubbed view for the comment form — a textarea and a submit button, with the submit button firing a request to the backend api.
- Create a submitComment action, which is wired up to pressing the submit button, a submitCommentSuccess action, which is fired when the submit request is successful, and a submitCommentFailure action, which is fired on request error with an error description.
- Create a reducer which, when fed with one of these actions and a current state value, returns the updated state value, representing the api request status.
- Wire up the comment form to the current state value to reflect the comment submit status.
This model is a simple and powerful tool for building and thinking about user interfaces. And it gave rise to a plethora of potential features and development tools — hot reloading, testability, saving and restoring the full state of the application, time-travelling, and so forth.
By contrast, Angular 1.x provides a failry complex, non-intuitive abstractions such as directives, fatories, services, controllers, etc. Its abstractions are much more complex and less intuitive than the ones in the previous model, thus it is harder to debug the code, and to make changes and introduce new features.
Elevating the thought process
Acquiring the skill of touch typing can eliminate then need to think about what symbols/words must be typed.
Learning VIM/Emacs removes the need to focus on the process of editing text, pushing it into muscle memory.
Picking the right library/approach/abstractions for the problem gives you more mental space to think about the task requirements, and mostly removes the need to think about the implementation details, the same way in which choosing the functional approach for tasks concerning data manipulation gives you space to think about the transformations needed, their composition and the generalizations/patterns of the solution.
Being aware of the abstraction level you are currently on, and the time you have been there, can give you a measure for assessing the quality of the higher-level decisions made, the quality of the language/tools used, and a red flag for exploring new ways of solving problems.