Introduction
Like many others, my employer has been pushing its workers to use AI as much as possible so that we might reach the promised land of 10-100x productivity. I have written about my views on generative AI before and will not rehash this here.
This short blog is meant to be a more positive read. The proliferation of AI-assisted or AI-led software engineering has sparked a desire in me to return, as they say, to simpler times. I have seen my colleagues flounder the moment Anthropic goes down. I have seen them build baroque edifices consisting of agents on top of agents on top of agents, all using sophisticated multi-tier memory systems (read: stacks of markdown files) that promise increasingly autonomous "agentic engineering". Oh, wait a second. I think I accidentally started ranting again. Let me retry.
Agency and mastery
A desire to return to simpler times, I said. What does that mean, exactly? In the core, I think, it is a desire for agency and mastery. Much of the recent developments in software engineering are focused on creating harnesses that take some goal, an indeterministic system, and a means of validating whether the goal has been reached; then, these systems iterate until the goal has been met. The less human intervention this process needs, the better. While I appreciate that for others, who might be more output-oriented than I am, this represents a step forward, for me, this is deeply uninteresting.
I first became interested in computers when I was fairly young; perhaps 12 or 13. I had access to a family computer, but not to the internet. Excited as I was after reading some Linux and programming related books, my parents generously allowed me to cordon off a virtual "play area" with a dual boot system, so that I could put my newly-gained knowledge into practice. Upon doing so, it became immediately obvious that I had merely scratched the surface; there was a sprawling, magical, and at times hermetic world of computing waiting to be explored - by me.
Bit by bit I pulled back the veil and learned about the inner workings. I vividly remember spending days, if not weeks, trying to wrap my head around pointers in C. Giving up, then being so annoyed at my inability to comprehend it that I had to go back and try again. Until finally something clicked and I got it.
This process of struggling with new concepts, slowly but surely building up a mental model until pieces of a puzzle start falling into place, brings me great joy. It allows me to lose myself in something that I am captivated by. But it's not only a matter of pleasure. In my experience, whenever you set out to truly learn something, it almost always snowballs into teaching you about a vast set of concepts that are adjacent to the initial thing you wanted to learn. What began as simply wanting to understand the difference between statements like int i = 0; and int* j = &i; very quickly demanded that I read about the basics of memory, array layouts, pointer arithmetic, memory initialisation, and many other concepts.
Some might argue that these are distractions, and I can, of course, see their argument. If you are working on a piece of software under the pressure of deadlines and potential adverse financial ramifications, one might not have the time to indulge in this kind of profound learning. In these cases, you might have to prioritise the business goals of your employer over your own intellectual development and, if you are lucky, you are paid a decent wage to do so.
However, I want to emphasise that very often, in our personal and professional lives, we could, if we tried, afford ourselves the space for this kind of engaged learning. At times, it's almost as though we feel guilty about allowing ourselves this luxury, afraid even briefly to cast off the yoke of "productivity" we so willingly tolerate. Who has the time to read documentation, let alone a book? This reticence and lack of confidence is precisely what tech companies are working so hard to manufacture. In order to justify their valuations and make their owners richer, they must make you dependent on them. They need you to choose to "just prompt Claude" over doing the hard work yourself to become proficient in a subject and widen your horizons. They want to reduce your agency to increase theirs. This is what their profit and power is predicated upon. Although I want to keep this blog focused on software engineering, this dynamic is by no means limited to that field. AI companies are churning out products that purport to "democratise" fields such as accounting, law, administration, and many others. "Just let Claude do it", so you don't have to (think anymore).
Despite the occasional temptation to turn off my brain too, I mostly quite like my agency. Incurring the cost of a tired brain seems a price worth paying to preserve my independence and ability to focus and think. So I am taking small steps to resist.
Those small steps for me consist mostly in returning to a very simple programming workflow; one that forces me to engage my brain, read documentation and code, and memorise (if I want to be productive) how to do things.
In recent weeks, I have started to teach myself Zig. The language is appealing to me for many reasons: it is a low-ish level system language, which I enjoy working with; it has interesting and unconventional characteristics such as the absence of implicit allocations and, since 0.16.0, the use of an explicit Io interface wherever I/O is performed, which broadens your horizon and makes you learn new concepts; and it is run by a non-profit organisation whose views on software are aligned with my own.
To further my goal of fostering my software engineering independence and developing (or, perhaps more accurately given the diminishing plasticity of my brain, "desperately maintaining") my capacity for learning new concepts in depth, I decided to learn Zig using what I consider to be a truly "old school" approach to software engineering. First, I needed a project. A friend of mine wanted to detect and delete duplicate files from his NAS which, despite the existence of tools that can already do this, felt like an ideal first serious-ish Zig project: duplik. In a previous blog article I have written about this specific project in more depth.
(Actually) writing code
All of the code for duplik (as well as this blog) is written using Vim. My Vim config installs a single plugin, fzf for fuzzy finding files/lines/tags, and sets a few defaults that work well for me. I do not use language servers or autocompletion:
call plug#begin()
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
Plug 'junegunn/fzf.vim'
call plug#end()
" editor settings
colorscheme sorbet
set termguicolors
set splitbelow
set splitright
set ignorecase
set smartcase
set showtabline=0
set hidden
set number
This is not (only) masochism: the more I get used to this bare-bones configuration, the more independent I become of a slew of third-party tools, plugins, specific versions of software, etc. I am reducing the things I need in order to do what I like, which is writing (sometimes useful) programs. An added benefit, which is both widely known and widely ignored, including, often, by me, is that writing things yourself (rather than tabbing through an autocomplete list) helps cement them into your brain. This is a known way to improve learning and long-term retention of knowledge and this ascetic config forces me to do it.
Navigating code without LSPs
Without a language server to serve up completions and provide hover hints, learning Zig means having to read the documentation. Reading documentation, in this economy? I know, crazy, isn't it? And although Zig's documentation is by no means bad, the language is under very active development and many APIs are as-yet unstable. This means that even documentation won't always cut it, requiring me to read the Zig source code itself. Thankfully the code is relatively easy to follow and well-documented.
This is where the absence of an LSP first started to annoy me a little. I kept navigating the Zig stdlib docs, reading the sources, then moving back to my editor. I work on a single screen, so there was a lot of window switching involved that took me out of my programming flow. I wanted to make this experience better without significantly increasing my dependency on more tooling. I liked the lack of autocomplete and the necessity to read documentation and code... I just wished I could do it from the comfort of Vim, jumping around my code and the Zig sources at will.
At this point I remembered that Vim natively supports tags-based navigation. These tags files are a low-tech index that maps language symbols, like function, type, or variable names, to the files and lines of code where they are defined. You can then jump from symbols to their definition; in Vim, you do this by pressing Ctrl-] while your cursor is on the relevant symbol. However, there were two barriers to using tags to aid my Zig development: universal ctags (a maintained version of ctags) does not support Zig language symbols out of the box, and it was not immediately obvious how to enable navigating to Zig stdlib symbols in addition to the symbols in my own project.
Then, when I had a spare weekend, I ended up building ztags which, to my surprise, even got linked on lobsters! ztags is a thin bash wrapper around ctags that automated the detection of a project's Zig version, downloads the relevant sources from https://ziglang.org, generates tags for the Zig source code itself, and appends those tags to the tags of your own project. This makes it seamless to navigate from your own code to Zig stdlib code within Vim (or emacs, for that matter), obviating the need for an LSP and allowing you to learn "from the source" as you develop your code. I don't think I've read as much source code in the past year as I have in the past couple of weeks alone.
The snowball effect
Remember how I wrote that when you set out to learn something, it often snowballs into teaching you about much more than the specific thing you wanted to understand initially? An observant reader may have recognised this thread already. My immediate motivation was to learn Zig while doing the "hard work" of reading documentation, hand-writing code, and, generally, not taking (too many) shortcuts. Because I chose a duplicate file detector tool as a vehicle for learning, I had to understand various approaches to this problem: multi-pass pipelines based on file sizes, partial content hashes, full content hashes, and the characteristics of different hashing algorithms; recursive file tree walking; the difference between symlinks and hardlinks; channel-based concurrency to parallelise file hashing; and many other concepts. Because I wanted to improve my code editing experience, I then built ztags which in turn led to brushing up my Bash scripting knowledge; understanding the tags format and the ctags CLI; reading about minisign and implementing a signature verification step for Zig source tarballs. And finally, as a result of opensourcing both duplik and ztags I ended up talking to members of the Zig (and wider programming) community, learning from them, and expanding my horizon and ideas further.
None of this would have happened had I "just asked Claude". I aim to take more of this mindset into my professional work, too. I am paid to design and produce good products within a reasonable time frame; not to turn off my brain or delegate my critical thinking skills. I will resist, politely where I can, the pressure to become a manager of a fleet of agents. Instead, I will work to become a better, more knowledgable, more independent programmer whose agency exceeds that of the LLMs. Surely that's worth something?