njms.ca

My principles are your technical debt

Published on 2023-12-26


Let me start with an example.

My Principles

At my last internship, we were building a React app that fetched some data from a server every time the page loads. Pretty standard stuff.

Using React hooks, that'd look something like this:

const component = (props) => {
	useEffect(() => {
		// Fetch the data
	}, []);

	return (/* Page content */);
}

Fetching the data from the server needs to happen on its own time so that the page can get rendered more quickly. To do that, we need the data fetch routine to be asynchronous; at first glance, something like:

// [...]

useEffect(async () => {
	// Fetch the data
}, []);

However, for reasons that would be better suited for a footnote¹, this would be wrong. The correct way to do this would be to have an asynchronous function that gets executed inside the useEffect hook, like:

// [...]

useEffect(async () => {
	(async () => {
		// Asynchronous data fetch operations
	})();
}, []);

For those blessed to have never needed to learn JavaScript, what's going on here is we're defining an anonymous, asynchronous function, and then immediately invoking it. This isn't the only way we could achieve this, but there's two main reasons why I'd be inclined to do it this way:

  • It reduces the clutter of the name space. If something doesn't need a name and only ever gets used once, I see no reason why we ought to give it a name
  • It keeps the relevant parts of the code together. If we were to stuff this routine in a function and store it elsewhere, maintainers would need to do more unnecessary jumping around to figure out how it works

This is in fact something I would have done a few times, but one day, it got flagged during code review. I don't remember what their specific reasoning was, but in hindsight, I can see a number of problems with it as well:

  • This "anonymous, asynchronous function invocation" pattern is terse
  • It's less common (that is, fewer people will look at this and immediately understand what it does)
  • Perhaps most relevantly, this was not the most common pattern for fetching page data in our code base. Usually we had a named function that went closer to the top of the file in the global name space

This is a problem I've been grappling with a lot over the last year or two; really since my first experience working on a code base with a large team: there are things I like to do while writing code because they feel "right," and there are things that make for the best experience in maintenance for the people around me. Let's think of some other examples.

Recursion

That is, writing functions that call themselves. I like writing recursive functions a lot. I think the main reason for this is that I personally like the experience of working in purely functional languages. It feels very elegant. More abstractly, I feel like thinking of problems recursively promotes some better behaviour than looping imperatively with a for or while loop.

However, the most significant problem with recursion² is that it's hard to reason about for a lot of people. Most people today are much more used to imperative programming, which relies a lot more on imperative loops. It seems like recursion just isn't as easy to introduce to new programmers who don't have an extensive math background, where recursion might feel more natural. So, it often takes a minute to figure out what recursive functions are actually doing.

Higher-order functions

I always try to jump on any opportunity I can get to write a higher order function. They're really cool, and they can clean up your code quite a bit. Higher order functions lend themselves very well towards compartmentalizing your code. They give you lots of the benefits of inheritance without needing to mess around with classes, which, in my opinion, don't feel as nice to work with.

However, it seems that most other people are a lot more familiar with the pattern of creating classes and inheriting from them. Object orientated programming tends to be introduced to a lot more people a lot sooner in their careers.

Monads are scary; or, The Queering of Object Orientated Programming

Efficiency

Efficiency is a high priority for me. I believe we lose out on a lot of the potential of computing when we waste our resources on processing abstractions.

However, those same abstractions are what makes computing as accessible as it is today. Often times, writing efficient code means using the syntax and structures less obviously suited to the job, making more esoteric design decisions, and overall making your code less accessible to maintainers. This isn't always the case, but it is more often than not.

Choosing the right language for the job

Some programming languages lend themselves very well to particular problems. Elixir and Erlang are great for creating robust back end applications. Python is great when you need something quick and easy, and you don't really care about performance. C++ is great when you do need to care about performance. Java is great if cross-platform compatibility is a major concern for your application.

But, not everyone knows the particular language you want to use for your project. On the other hand, JavaScript is known by quite a few developers these days and will... technically... do whatever you want it to, given a strong enough computer. It's a widely known programming language with a low barrier of entry, a friendly syntax, a wide user base and a really diverse set of domains you can technically ram it into given enough gumption and/or self-loathing.

I could go on like this for quite a while, but I think by now you're starting to see a pattern.

Your technical debt

I have a sense of what it means to write good code, and I know what kind of code I need to write in practice. Boring, slow code will always trump what I think of as elegant, clean code in a world where we optimize for maintainability and general developer experience.

My principles are your technical debt.

The principles I've picked up don't exist in a vacuum. I've learned them over the years mainly by reading about other software developers' practices. Many of us grew up on the stories of hackers from the 80s and 90s who wrote in today's "dark arts" of computer programming: C, x86 assembly, FORTRAN, Perl... the kind of stuff we avoid today like the plague. This seems especially the case for my colleagues and I in web development.

The heroes of these stories worked on computers that didn't have virtually infinite memory. Their programs were smaller out of necessity. They worked a lot closer to the metal, so to speak, and had a more intimate relationship with the machines they programmed.

As for functional programming, what we see today feels a lot like the vestiges of a dream that, while it may have materialized, never seemed to take over the world in the way its dreamers may have liked. A vision of a purer, more mathematical model of computing. A more physical manifestation of the lambda calculus. Lisp machines. Immutability everywhere. These things all exist in their respective corners of programmer culture, but you'd be hard pressed to find an employer who'd pay you money to write Scheme. They exist, but they're much, much harder to find than people who employ, say, JavaScript developers.

I've been holding off on using the word craft to describe software upholding these traditions, since many people have discussed this at a much greater length than I have. Just recently, I've read two articles on the subject:

"Whatever happened to programming? by Mike Taylor

"Reverence for the Craft, Business, and Your Immortal Soul" by Ludic

The latter is particularly interesting in how it brings up Robert Pirsig's Metaphysics of Quality, which is an idea that gets developed in his book "Zen and the Art of Motorcycle Maintenance". I just finished reading that book the other night, so I feel particularly inclined to invoke it myself.

In short, and based on my own reading, Quality is something we all know intuitively but will necessarily struggle to define rigorously because it exists beyond the scope of science and objectivity. More specifically, Pirsig believed that Quality exists before we think about stuff and sort into little boxes as we do. Those ideas we hold about the things in our lives and in the world, they're formed as we interact with Quality. It's our relationship to Quality that shapes our view of the world.

Importantly, the unnamed narrator of "Zen and the Art of Motorcycle Maintenance" talks at length about a person named Phaedrus, which is revealed to be who the narrator was before being declared insane and subjected to an involuntary treatment where he received an electric shock to the brain.

Phaedrus believed that these things he was figuring out about Quality were so far outside of the scope of the way we think here in the West that to fully understand it, to fully develop his Metaphysics of Quality, would be to go insane. When he did, the state agreed.

Despite how we all intuitively understand what it means for something to be "high Quality," to truly live a life of Quality is heretical. Instead, we're resigned to be surrounded by things with only a surface-level of beauty.

Creating software is all about accepting trade-offs, but in my experience, the only thing we seem to be optimizing for these days is what we might call developer efficacy. Not necessarily developer happiness, but rather, creating an environment where developers are most able to create new and maintain existing software. How can we maximize the rate at which new software can be developed? How quickly can we get new software developers comfortable working on a particular project?

Making this your number-one priority doesn't make for high Quality software. Rather, it makes for lots of okay software; stuff that looks good on the surface and paints over internal complexity.

"Insane" is a loaded word, mainly because it's used to put down people who are mentally ill, but also because it paints lines around what you're allowed to think (that is, what constitutes "normal" thought) and which experiences are "wrong."

The problem I seem to have is that most of the software we have today is coming out of industry. It's industry that paints that line, and industry doesn't need Quality software. In fact, it needs lots of okay software! So, prioritizing this developer efficacy makes sense. It's a good business decision.

In that way, React is to front end web development what the spinning jenny was to pre-industrial textile manufacturing. I don't think we've ever lived in a world where software development was a cottage industry per se, although open source software development might be close, but I don't think it should be too surprising to see parallels between the ways old-fashioned software developers feel about modern abstractions and the resentment of the new ways among traditional cloth makers.

There's a lot of problems with this kind of software. Importantly, I think this approach to software development creates new kinds of problems in a way that the transition to industrial cloth manufacture didn't. But, this isn't an article about software bloat; I want to write about software craft.

Making Quality software is such a positive experience because it connects with us in a way that's lost when we only prioritize its longevity. It reminds us of why we got into this whole computer thing in the first place. This is why I suspect so many people I run into online describe their experience creating software and interacting with it as "soulless." It's missing the soul of its developer.

Transience-orientated programming, or how to make software for ghosts

A better principle

I don't think there really is a "solution" to this problem. Not without replacing the whole system governing the behaviour. As I said, this seems to have less to do with attitude than structure. The industry does not and probably never will prioritize Quality in the way "artisan" software developers want.

I do think there might be a tiny compromise we can make, though.

What if we choose consistency over developer efficacy?

Consistency is, in a way, an aspect of developer efficacy. It is easier to create and maintain software that consistently uses the same patterns. But if we deprioritize making everything as easy as possible, then we make some room to pick and choose some key battles.

Let's say there's a more domain-specific programming language that is better suited for a particular project than what your team already knows. Even if getting your team to learn this new language would be a bigger up-front cost, there's two key benefits that'll pay off in the long term.

For one, learning new programming languages can introduce you to novel ways to solve complicated problems. You can bring these ideas back to whatever technologies fit in your comfort zone in ways that might greatly improve the Quality of your software. This is kind of what I did with Haskell. I was really into Haskell for a year a while back. I had a hard time figuring out how to approach larger projects with it so I didn't get very far, but working with recursive functions, partial application, higher order functions, and it's advanced type system have greatly informed the way I write code today.

Besides that, there are clear benefits to using relevant technologies in their relevant domains! PHP was designed to run on the server. JavaScript was not. There are layers of abstraction in place to get JavaScript to work on the server that PHP doesn't need. PHP has design decisions that make it naturally well-suited to run on the server. It's nice when there's a consonance between what a language is for and where it's being used.

(You can tell I've spent the better part of my life writing JavaScript, I'm sure)

What matters, then, is that when you make that upfront investment, you stick with it. You adapt, and then similar investments you need to make in the future become a little easier.

I suppose that's what they call growth.

Afterword

As with a lot of the things I write, I wonder if I need to take a moment to talk about pretentiousness.

Usually I try my best to avoid it, since it doesn't add a lot to the piece. I care about stuff. I like writing about the stuff I care about. Some people don't care about these things, and so they may find my writing a little pretentious. I'm mostly at peace with that, I think. If people don't like what I write they're more than welcome not to read it.

There's lots of people who'd call this fixation on artisanship and craft in software development pretentious, and there's some compelling reasons why. Today, writing software is more accessible than ever before. Usually, if someone thinks writing software is above them, it's probably because they've been trained to believe that they're intrinsically not smart enough to do cool computer things. They were wronged.

Writing software in the 90s was a lot harder, and the practice was reserved to far fewer people because of it. Usually, these people were men with a university education. In particular, wealthy men whose families could afford computers when they were growing up.

Further to this point, I have a hard time talking to most of the people in my life about this sort of thing because I don't hang around "technical" crowds very often. Many people I know in university grew up on Windows XP and seem to share the nostalgia for old software, but for the most part, it's hard to avoid the fact that user experience practices have come really far in recent years. To most people, today's software is better than ever before³.

Fundamentally, this is inside ball, and I think that's worth bringing up. This is about individuals' experiences with making software. One can live a long and happy life not writing software for the industry, and if it's too soul-crushing, you can always do what so many others seem to be doing these days and find another line of work. The tech industry has no end of weird computer things that need to be worked on. I haven't even graduated from university yet and I'm already starting to see software development as more of a hobby⁴.

It's sad though, to feel compelled to leave software development, having discovered that it's not what it was promised to be. I think that, more than anything, is what inspired me to go all in on free software: a community that seems to treat all my principles as high priorities.

Footnotes

¹ useEffect takes a set up function and a list of dependencies. The setup function can't just be any function; it's a function that's expected to return either an object describing some clean up actions, or nothing. async functions return a Promise, which is unexpected, and causes problems. For more information, see the React reference documentation for useEffect.

useEffect (react.dev)

² Admittedly, an equally large or larger problem with recursion is that many programming languages don't handle it very well. Recursion isn't all that memory efficient, unless you're doing the special case of tail-recursion, which may sometimes get optimized to be as efficient as imperative looping. JavaScript generally does not optimize tail-recursion. Purely functional languages which rely on recursion usually do.

Are functions in JavaScript tail-call optimized? (stackoverflow.com)

Efficiency is a whole other problem I touch on later.

³ This is starting to change now that three quarters of your every web request is just ads.

⁴ Hit me up if you're aware of any internships or junior positions in system administration!

Respond to this article

If you have thoughts you'd like to share, send me an email!

See here for ways to reach out