Simo Virokannas

Writings and ramblings

Transference of Complexity

Summary

Programming languages have evolved in accessibility, complexity and readability in the past six decades. The overall internal complexity of a single software application has grown exponentially, while the amount of code lines written to achieve a specific end result has decreased.

This naturally occurring transference of complexity brings about completely new kinds of problematic design patterns and software rot. This article tries to document some of them and proposes some mitigation methods.

Complexity

Complexity, in the context of this article, is the total number of branches and other run control structures necessary to implement the desired functionality.

Language, framework and toolset

This is the combination of the programming language, the framework(s) that are available and selected to be used, and the development toolset.

Each one of these three will make choices available for the user who is writing code.

Languages may use different terminology for what a framework is:

  • Python uses “modules”, provided by installable or interpreter-provided packages
  • Swift uses importable “frameworks”, provided through custom or system-provided packages
  • C and C++ use “libraries” that can be either statically or dynamically linked
  • Ruby: gems
  • Node.js, C#: packages

In addition to language and frameworks, the toolset may use a precompiler or other mechanisms to further minimize the need for user-written code.

Minimal user-code complexity

This is the least possible amount of complexity needed in user-written code to perform each step. If the language provides a pre-packaged method to do exactly what’s needed, that is the absolute minimal complexity.

The delimiter between the two

There is an optimal complexity delimiter between the language/framework/toolset and user code. It might not be obvious or documented but it exists. This is also the sweet spot where it’s easy to work with the code, easy to bring in new people to work on a project, and has a minimal need of continuous refactoring.

Transference

Within a language

As languages are developed further, transference may happen within the language itself. A good example of this is optional chaining.

Unlike some newer languages like Swift and C#, Javascript originally had no true optional/null coalescing operator. This was only added in the ECMAScript 2020 standard. Before that, in some cases it was necessary to perform additional checks to find out if a value was truly null (and not just zero, false or an empty string) before coalescing to the right-hand option:

function coalesce(value, defaultValue) {
 if (value == null || !(value === value))
   return defaultValue;
 return value;
}

let result = coalesce(possiblyNull, defaultValue);

This means that adopting a newer standard, one could do:

let result = possiblyNull ?? defaultValue;

The earlier code involved entering a subroutine and branching therein, resulting in more complex code and reduced readability (try choosing a good readable name for the coalesce function).

After the language introduced a null coalescing operator, that turned into a single readable and easily understandable statement.

By switching frameworks

For example, using Python (language) with urllib3 (framework) to make a simple URL request and parsing a JSON response involves more steps than using Python with requests.

import urllib3
import json
http = urllib3.PoolManager()
request = http.request("GET", "https://just.an-example.com/api")
response = json.loads(request.data.decode("utf-8"))

vs.

import requests
response = requests.get("https://just.an-example.com/api").json()

The requests module provides a shorthand .get method for a GET request, and its response object has a json() method to handle the output conversion to a dictionary or array object. The two above examples have the same functionality, same level of error handling (none whatsoever) and can be expected to behave the same way, but the complexity of handling the connection pool, decoding the data stream to utf-8 and parsing the JSON object have been moved to the framework.

Writing an asynchronous call and handling its result using Swift 2 is a more involved process than in Swift 5. Writing a line of text to a terminal using vanilla C is different than C++ and iostream.

In all these cases, the overall complexity still stays the same, as the code still has been written, but doesn’t need to be the problem of the person writing this specific application.

Through the toolset

Some toolsets provide a precompilation step (like C and C++) or boilerplate code that can then be extended by the user (Swift/ObjC + CoreData). Additionally, there may be tools to be used with a framework to dynamically regenerate code within existing source files. UI frameworks like Qt and WxWidgets might either provide these tools (e.g. uic for Qt) or others may have written tools on top of them (e.g. Code::Blocks).

This moves sometimes massive amounts of complexity from the hands of the user into a configuration file and the toolset itself.

Problem categories

Design patterns

To be clear, the goal isn’t to imply that a design pattern would be wrong, either as-is or within a certain context, but instead that due to observed historical transference of complexity some patterns may lead to problems later on.

Example: Observer + Reference counting, but not aware of each other

If you’re working in an environment where these two are paired, good on you. In many cases this is not a given.

Not all languages started with reference counting built-in. Some purposefully don’t include it. Many of the “modern” languages assume you’re familiar and comfortable with it.

The problem arises when an observer subscribes to a notifier and thus creates a reference to itself in the notifier’s context. If this link is not properly severed, all of these observers will remain resident in memory long after they’ve outlived their usefulness. A design pattern that’s supposed to make the control flow easier now is both slowing the application down incrementally (since all the observers will still be called) and memory use will creep up.

Some languages use the concept of a weak reference to mitigate this issue, but that still leads to a degree of incremental slowdown paired with other control flow branch: the called method now needs to check if the owner still exists.

For successfully using such combination of design patterns, yet another mechanism is needed to remove observers from the queue as they’re freed.

The Observer + RC problem has to do with transference of complexity in two ways:

  • A developer might move from a language that didn’t have reference counting to one that does, while being used to using the observer pattern
  • A language might include automatic reference counting after the fact (as happened with Objective-C) and this problem is not obvious or automatically addressable

Software rot on arrival

A developer may be used to writing software in a specific way, even have some neat tricks up their sleeve, especially if having written code for decades using different languages. This may be working to their disadvantage. Newer languages may have already transferred that complexity out of user code, but the developer can still write in their preferred way.

Developers are drawn to complexity like moths to a flame, often with the same outcome.

Neal Ford in “The Productive Programmer”

A couple of examples:

  • Passing information in and out of methods used to be a much heavier operation than it is in modern languages that treat almost all data as objects. This led to a tendency of keeping and treating “expensive” data within a single method, with deep branching and looping for hundreds and hundreds of lines of code. There are really no languages that would prohibit this. Most modern languages only operate on stack and passing chunks of data has little to no performance or memory cost. Reading and trying to understand a 800-line method has significant cost.
  • Many older languages don’t have the concept of blocks or enclosures, which contributed to the popularity of the observer pattern and delegate protocols. Bringing these concepts to languages that have a built-in alternative leads to having to read through several source files, class declarations and functions to simply understand what may happen due to a notification being sent – instead of passing the implementation as a block from the context that is aware of what needs to happen.

Mitigation

Read

When you go from one language or framework to another, take the time to read what makes it different. Adopt its strengths. Every programming language and new framework was born out of frustration with a goal to reduce that frustration, not increase it. If the design patterns it provides out of the box are documented, read that documentation.

Be complexity-conscious

It may take time to figure out where the overall delimiter between user-code complexity and indirect complexity lands when a new project starts, but when it is determined, document it within the project and update that document whenever it moves.

Conclusions

  • Indirect (or involuntary) complexity is determined by the language, framework and toolset
  • Minimal user-code complexity is therefore quantifiable
  • It’s possible to find out the delimiter between these two
  • Transference of complexity will happen naturally over time, so that delimiter needs to be revised periodically
  • Switching from a language, toolset or framework to another, finding the delimiter is of key importance

Comments

One response to “Transference of Complexity”

  1. Mike Sanders

    Nice article! I especially liked the part about learning a new language’s strengths: “When you go from one language or framework to another, take the time to read what makes it different. Adopt its strengths. Every programming language and new framework was born out of frustration with a goal to reduce that frustration, not increase it. If the design patterns it provides out of the box are documented, read that documentation.” Many devs ignore this and keep banging their head against old ways of doing things because it’s what they’re used to. They don’t consider that other devs have, like you said, created this language because of frustrations with existing languages.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.