It seems traditionally programming is seen as an activity where you make the computer do what you want. You have some requirements for the program, and then you write code to make it do the thing. It’s a linear “one way” process which you repeat until the program works.
But I don’t think it’s a linear one way progress, where you only insert things into the computer. Instead, it’s a feedback loop, where the programming language and the programming environment actively shapes your idea and approach, in response to how you write your program.
This post is an exploration of the idea that programming languages and related tools affect how we write code. How they provide feedback to us, and how this changes the way we express ourselves in code, and how this ultimately affects things like how easy a program is to maintain or extend.
Intelligent activity feeds back to your understanding of it
Programming languages and other related tools provide you feedback as you use them. There are two obvious examples: You write code, try to run it, and find out it doesn’t compile. Or, you write code, try to run it, and find out it doesn’t do what you wanted. I think it’s clear in these cases how the language “provides feedback” - The error or other failure tells you that you need to write your code in a different way. But it seems there’s more to it than just this. I think the tools also guide you towards what kind of architecture and design you should be using.
This kind of goes back to what Gilbert Ryle writes about intelligent activity in his book The Concept of Mind (I previously wrote about the book here). Ryle suggests “intelligent activity” is when you do some activity, and then incorporate feedback back into your ability to do the activity. For example, if you’re throwing a basketball, you consider how much force you put into your throw, what angle you’re throwing it at, and so forth - and depending on whether or not the ball goes into the hoop, you adjust accordingly.
If we break programming down to the fundamental level, programming is about expressing your ideas in code. You have some idea or concept that you translate into code, which becomes a representation of your idea. What I mean by “idea” or “concept” could be a lot of different things, like “login page” or “image compression” or “render a polygon”, or anything else that you can use as a starting point for writing code.
The crucial point is that the code is a representation of the idea, so it’s not the same as the idea itself - the idea can be represented in multiple ways, where some might be better and some worse. For example, we could use different a different class or function structure to achieve the same result.
We could compare this to painting: There are multiple ways you can paint a particular picture, ranging from the types of paints used, the color choices, and stylistic decisions. None of them are necessarily the “right way” to do it, but some might be more suitable for a particular purpose, or to convey a particular meaning. The same applies to code, with trying to convey a particular meaning being particularly important.
A programming language sets constraints on how you can represent your ideas using it. For example, in an object-oriented language, you typically represent your ideas as objects containing properties and methods. In a functional language, you might represent them as functions which operate on immutable data. Similarly, libraries and tools have the same effect, such as the format you store your data in usually depends on what type of database you’re using.
These constraints shape your idea: As you start writing code, you often find out you can’t express it quite like you wanted to. You find that you might adjust your original idea in some way to make it work better in the context of your program’s requirements, or because the way some library you use works. In other words, the programming language and environment provides feedback on your idea, which reshapes it, which then further reshapes how you write the code to represent it.
As an example, if you’ve worked on non-trivial programs, you might’ve noticed situations where some particular piece of code feels clunky. This could be the language giving you feedback on your solution, telling you that it isn’t quite what it should be. Similarly, code which is hard to follow is often a sign that it doesn’t represent the idea in a way that fits the language. But the problem is that it can sometimes be hard to identify what the problem is, and what approach should be used instead. Nevertheless, it’s feedback that shouldn’t be ignored.
I wrote an elaborate class-hierarchy for our codebase at work to solve a particular problem. It made me feel very clever (which should have been a warning sign). Over time, certain parts of the code started becoming increasingly complicated, with more and more edge-cases stuffed into the logic. Code using the classes became harder to write, with more bugs occurring because of aforementioned edge-cases and how complex it was to handle it. A coworker had suggested using a much simpler FP-inspired approach. I gave up trying to make my cool class system work and we went with his idea instead.
It’s in this way that programming works as a feedback loop between the programmer and the programming language (and related tooling). The way I tried to force the language to represent my idea just didn’t work and it was increasingly visible in the feedback, in how complicated and clunky things started becoming. When we moved into the other approach, everything worked smoothly, even if it did mean we had to call one or two more functions explicitly in places.
Understanding the feedback loop lets us write better code
One reason why understanding this feedback loop is useful is that we can use it to our advantage when we build software. We can build feedback mechanisms like this into our own code, which helps preserve the design/architecture, and helps communicate the design intent.
A common problem in longer-running projects with multiple developers is that the code is changed in ways that’s incompatible with the original design intent. But if we build our code in a way that gives feedback to the person trying to use it, we can help them make the right choices.
A good example of this is using an Optional data type when a function might return nothing. When I attempt to call the function without handling no result, I get immediate feedback: I should handle the no result case. When I try to modify the function, I also get immediate feedback: This function intentionally returns no value sometimes, it isn’t an accident caused by a sloppy implementation. The Optional type also stresses the importance of handling the no result case, especially in languages where it’s entirely valid to just return null or such. Similarly, we can use non-nullable types, immutable functions, how classes can be extended, and other features to intentionally create constraints and rules the user of the code has to follow.
This becomes useful especially when building code to solve stable problems. This often takes the form of library code, which is used by other parts of your application. If you design your code in a way that the person using it gets feedback from the code, it’s likely their solution will be better and more in line with how you intended the library to be used.
Different languages encourage expressing your ideas differently
Another useful thing to note is that different programming languages encourage expressing your ideas differently.
Attempting to implement a web server is likely to result in quite a different architecture if done in Elixir compared to Node.js. The way Elixir encourages you to write these types of programs puts reliability front and center, unlike Node.js, which is likely to crash a lot. If we care about reliability, a language like Elixir provides feedback on our ideas to make them more reliable.
A strongly typed language generally requires you to be more explicit about how you declare your data types. This often means the design intent is once again more explicitly being communicated, because you can’t just slap more stuff into a data type without at least taking a glance at what it looks like. A weakly typed language on the other hand requires you to take much more active measures to ensure these types of ideas are expressed clearly.
This is why it’s extremely useful to be familiar with different programming languages, especially languages which are different from the others you already know. They show you different ways the same ideas can be represented in code, which is often very useful even if you’re not using that specific language. But for some reason, it seems nowadays especially those with a bit less experience seem wary of trying out different languages. I credit much of my programming ability to the fact I’ve tried out many different languages, even if I never made anything particularly useful with most of them.
This post began as an experiment: Can I take a random note I have in Obsidian, and expand upon the idea in it? Being able to write more is one of the supposed benefits of using a Zettelkasten-style notetaking system, which is something I’ve been doing. I guess it works.
Comments or questions?
If you have any comments or questions about this post, feel free to email me to jani@codeutopia.net, or use any of the other methods on the contact page.