Thursday, October 17, 2024

Programming

Some programming is routine. You follow the general industry guidelines, get some libraries and a framework, and then whack out the code.

All you need to get it done correctly is a reasonably trained programmer and a bit of time. It is straightforward. Paint by numbers.

Some programming is close to impossible. The author has to be able to twist an incredible amount of details and complexity around in their head, in order to find one of the few tangible ways to implement it.

Difficult programming is a rare skill and takes a very long time to master. There is a lot of prerequisite knowledge needed and it takes a lot to hone those skills.

Some difficult programming is technical. It requires deep knowledge of computer science and mathematics.

Some difficult programming is domain-based, it requires deep knowledge of large parts of the given domain.

In either case, it requires both significant skill and lots of knowledge.

All application and system programming is a combination of the two: routine and difficult. The mix is different for every situation.

If you throw inexperienced programmers at a difficult task it will likely fail. They do not have the ability to succeed.

Mentorship is the best way that people learn to deal with difficult programming. Learning from failures takes too long, is too stressful, and requires humility. Lots of reading is important, too.

If it is difficult and you want it to work correctly, you need to get programmers who are used to coping with these types of difficulties. They are specialists.

Thursday, October 10, 2024

Avoidance

When writing software, the issues you try to avoid often come back to haunt you later.

A good example is error handling. Most people consider it a boolean; it works or does not. But it is more complicated than that. Sometimes you want the logic to stop and display an error, but sometimes you just want it to pause for a while and try again. Some errors are a full stop, and some are local. In a big distributed system, you also don’t want one component failure to domino into a lot of others, so the others should just wait.

This means that when there is an error, you need to think very clearly about how you will handle it. Some errors are immediate, and some are retries. Some are logged, others trigger diagnostics or backup plans. There may be other strategies too. You’d usually need some kind of map that binds specific errors to different types of handlers.

However, a lot of newer languages give you exceptions. Their purpose is that instead of worrying about how you will handle the error now, you throw an exception and figure it out later. That’s quite convenient for coding.

It’s just that when you’ve done that hundreds of times, and later never comes, the behavior of the code gets erratic, which people obviously report as a bug.

And if you try to fix it by just treating it as a boolean, you’ll miss the subtleties of proper error handling, so it will keep causing trouble. One simple bit of logic will never correctly cover all of the variations.

Exceptions are good if you have a strong theory about putting them in place and you are strict enough to always follow it. An example is to catch it low and handle it there, or let it percolate to the very top. That makes it easy later to double-check that all of the exceptions are doing what you want. It puts the handling consistently at the bottom.

But instead, many people litter the code indiscriminately with exceptions, which guarantees that the behavior is unpredictable. Thus, lots of bugs.

So, language features like exceptions let you avoid thinking about error handling, but you’ll pay for them later if you haven’t braced that with enough discipline to use them consistently.

Another example is data.

All code depends on the data it is handling underneath. If that data is not modeled correctly -- proper structures and representations -- then it will need to be fixed at some point. Which means all of the code that relies on it needs to be fixed too.

A lot of people don’t want to dig into the data and understand it, instead, they start making very loose assumptions about it.

Data is funny in that in some cases it can be extraordinarily complicated in the real world. Any digital artifact for it then will have to be complicated. So, people ignore that, assume the data is far simpler than it really is, and end up dealing with endless scope creep.

Incorrectly modeled data diminishes any real value from the code, usually a whole collection of bugs. It is the foundation, which sets the base quality for everything.

If you understand the data in great depth then you can model it appropriately in the beginning and the code that sits on top of it will be far more stable. If you defer that to others, they probably won’t catch on to all of the issues, so they over-simplify it. Later, when these deficiencies materialize, a great deal of the code built on top will need to be redone, thus wasting a massive amount of time. Even trying to minimize the code changes through clever hacks will just amplify the problems. Unless you solve them, these types of problems always get worse, not better.

Performance is an example too.

The expression “Premature optimization is the root of all evil” is true. You should not spend a lot of time finely optimizing your code, until way later when it has settled down nicely and is not subject to a lot of changes. So, optimizations should always come last. Write it, edit it, test it, battle harden it, then optimize it.

But you can also deoptimize code. Deliberately make it more resource-intensive. For example, you can allocate a huge chunk of memory, only to use it to store a tiny amount of data. The size causes behavioral issues with the operating system; paging a lot of memory is expensive. By writing the code to only use the memory you need, you are not optimizing it, you are just being frugal.

There are lots of ways you can express the same code that doesn’t waste resources but still maintains readability. These are the good habits of coding. They don’t take much extra effort and they are not optimizations. The code keeps the data in reasonable structures, and it does not do a lot of wasteful transformations. It only loops when it needs to, and it does not have repetitive throwaway work. Not only does the code run more efficiently, it is also more readable and far more understandable.

It does take some effort and coordination to do this. The development team should not blindly rewrite stuff everywhere, and you have to spend effort understanding what the existing representations are. This will make you think, learn, and slow you down a little in the beginning. Which is why it is so commonly avoided.

You see these mega-messes where most of the processing of the data is just to pass it between internal code boundaries. Pointless. One component models the data one way, and another wastes a lot of resources flipping it around for no real reason. That is a very common deoptimization, you see it everywhere. Had the programmers not avoided learning the other parts of the system, everything would have worked a whole lot better.

In software development, it is very often true that the things you avoid thinking about and digging into, are the root causes of most of the bugs and drama. They usually contribute to the most serious bugs. Coming back later to correct these avoidances is often so expensive that it never gets done. Instead, the system hobbles along, getting bad patch after bad patch, until it sort of works and everybody puts in manual processes to counteract its deficiencies. I’ve seen systems that cost far more than manual processes and are way less reliable. On top of that, years later everybody get frustrated with it, commissions a rewrite, and makes the exact same mistakes all over again.

Thursday, October 3, 2024

Time

Time is often a big problem for people when they make decisions.

Mostly, if you have to choose between possible options, you should weigh the pros and cons of each, carefully, then decide.

But time obscures that.

For instance, there might be some seriously bad consequences related to one of the options, but if they take place far enough into the future, people don’t want to think about them or weigh them into their decisions. They tend to live in the moment.

Later, when trouble arises, they tend to disassociate the current trouble with most of those past decisions. It is disconnected, so they don’t learn from the experiences.

Frequency also causes problems. If something happens once, it is very different than if it is a repeating event. You can accept less-than-perfect outcomes for one-off events, but the consequences tend to add up unexpectedly for repetitive events. What looks like an irritant becomes a major issue.

Tunnel vision is the worst of the problems. People are so focused on achieving one short-term objective that they set the stage to lose heavily in the future. The first objective works out okay, but then the long-term ones fall apart.

We see this in software development all of the time. The total work is significantly underestimated which becomes apparent during coding. The reasonable options are to move the dates back or reduce the work. But more often, everything gets horrifically rushed. That resulting drop in quality sends the technical debt spiraling out of control. What is ignored or done poorly usually comes back with a vengeance and costs at least 10x more effort, sometimes way above that. Weighted against the other choices, without any consideration for the future, rushing the work does not seem that bad of an option, which is why it is so common.

Time can be difficult. People have problems dealing with it, but ignoring it does not make the problems go away, it only intensifies them.

Thursday, September 26, 2024

Complexity

All “things” are distinguished by whether they can be reasonably decomposed into smaller things. The lines of decomposition are the similarities or differences. Not only do we need to break things down into their smallest parts, but we also need to understand all of the effects between the parts.

Some ‘thing’ is simple if it defies decomposition. It is as broken down as it can be. It gets more complex as the decompositions are more abundant. It is multidimensional relative to any chosen categorizations, so it isn’t easy to compare relative complexity. But it is easy to compare it back to something on the same set of axes that is simpler. One hundred things is marginally more complex than five things.

This is also true of “events”. A single specific event in and of itself is simple, particularly if it does not involve any ‘things’. Events get more complex as they are related to each other. Maybe just sequential or possibly cause and effect. A bunch of related events is more complex than any single one. Again it is multidimensional based on categorizations, but also as events can involve things, this makes it even more multidimensional.

For software, some type of problem exists in reality and we have decided that we can use software to form part of a solution to solve it. Most often, software is only ever a part of the solution, as the problem itself is always anchored in reality, there have to be physical bindings.

Users for instance are just digital collections of information that represent a proxy to one or more people who utilize the software. Without people interacting or being tracked with the software, the concept of users is meaningless.

Since we have a loose relative measure for complexity, we can get some sense of the difference between any two software solutions. We can see that the decomposition for one may be considerably simpler than another for example, although it gets murky when it crosses over trade-offs. Two nearly identical pieces of software may only really differ by some explicit trade-off, but as the trad-offs are anchored in reality, they would not share the same complexity. It would be relative to their operational usage, a larger context.

But if we have a given problem and we propose a solution that is vastly simpler than what the problem needs we can see that the solution is “oversimplified”. We know this because the full width of the solution does not fit the full width of the problem, so parts of the problem are left exposed. They may require other software tools or possibly physical tools in order to get the problem solved.

So, in that sense, the fit of any software is defined by its leftover unsolved aspects. These of course have their own complexity. If we sum up the complexities with these gaps, and if there were any overlaps, that gives us the total complexity of the solution. In that case, we find that an oversimplified solution has a higher overall complexity than a properly fighting solution. Not individually, but overall.

We can see that the problem has an ‘intrinsic’ complexity. The partial fit of any solution must be at least as complex as the parts it covers. All fragments and redundancies have their own complexity, but we can place their existence in the category of ‘artificial’ complexity relative to any better-fitting solution.

We might see that in terms of a GUI that helps to keep track of nicely formatted text. If the ability to create, edit, and delete the text is part of the solution, but it lacks the ability to create, edit, and delete all of the different types of required formatting then it would force the users to go to some outside tool to do that work. So, it’s ill-fitting. A second piece of software is required to work with it. The outside tools themselves might have an inherent intrinsic complex, but relative to the problem we are looking at, having to learn and use them is just artificial complexity. Combined that is significantly more than if the embedded editing widget in the main software just allowed for the user to properly manage the formatting.

Keep in mind that this would be different than the Unix philosophy of scripting, as in that case, there are lots of little pieces of software, but they all exist as intrinsic complexity ‘within’ the scripting environment. They are essentially inside of the solution space, not outside.

We can’t necessarily linearize complexity and make it explicitly comparable, but we can understand that one instance of complexity is more complex than another. We might have to back up the context a little to distinguish that, but it always exists. We can also relate complexity back to work. It would be a specific amount of work to build a solution to solve some given problem, but if we meander while constructing it, obviously that would be a lot more work. The shortest path, with the least amount of work, would be to build the full solution as effectively as possible so that it fully covers the problem. For software, that is usually a massive amount of work, so we tend to do it in parts, and gradually evolve into a more complete solution. If the evolution wobbles though, that is an effort that could have been avoided.

All of this gives us a sense that the construction of software as a solution is driven by the understanding and controlling of complexity. Projects are smooth if you understand the full complexities of the problem and find the best path forward to get them covered properly by a software solution as quickly as possible. If you ignore some of those complexities, inherent or artificial, they tend to generate more complexity. Eventually, if you ignore enough of them the project gets out of control and usually meets an undesirable fate. Building software is an exercise in controlling complexity increases. Every decision is about not letting it grow too quickly.

Sunday, September 22, 2024

Editing Anxieties

An all too common problem I’ve seen programmers make is to become afraid of changing their code.

They type it in quickly. It’s a bit muddled and probably a little messy.

Bugs and changes get requested right away as it is not doing what people expect it to do.

The original author and lots of those who follow, seek to make the most minimal changes they can. They are dainty to the code. They only do the littlest things in the hopes of improving it.

But the code is weak. It was fully thought out; it was poorly implemented.

It would save a lot of time to make rather bigger changes. Bold ones. Not to rewrite it, but rather to take what is there as a wide approximation to what should have been there instead.

Break it up into lots of functions.

Rename the variables and the function name.

Correct any variable overloading. Throw out redundant or unused variables.

Shift around the structure to be consistent, moving lines of code up or down the call stack.

All of those are effectively “nondestructive” refactoring. They will not change what the code is doing, but they will make it earlier to understand it.

Nondestructive refactors are too often avoided by programmers, but they are an essential tool in fixing weak codebases.

Once you’ve cleaned up the mess, and it is obvious what the code is doing, then you can decide how to make it do what it was supposed to do in the first place. But you need to know what is there first, in order to correctly change it.

If you avoid fixing the syntax, naming, inconsistencies, etc it will not save time, only delay your understanding of how to get the code to where it should be. A million little fixes will not necessarily converge on correct code. It can be endless.