Why It's Everywhere
Everyone has a different opinion on why AI is everywhere. Here’s mine.
The shift towards natural-language-oriented-programming is the latest step in the decades-long abstraction battle that all programming languages have been fighting over. We increasingly create languages and conventions that are supposed to give us the power to express things quicker, with less effort. It’s why you’ll see billion dollar companies running million dollar workloads on Python. But anyone dealing with performance-critical code will quickly realize that most of these higher-level abstractions often end up getting in the way of what we’re actually trying to do. Often, they just act as an annoying barrier that we have to break down anyway to figure out what’s really going on underneath.
Programming is, unfortunately, no longer about writing programs. We sit down not to figure out how our CPU will solve a problem, but rather to figure out which libraries are best fit for our application, which cloud constructs will fit our scale, which programming paradigms are idiomatic. These questions often take us away from the point of writing the program altogether; we spend our time reading the React documentation and sketching inheritance diagrams instead.
And the argument is simple to understand: we can save time by building and reusing these abstractions to solve well-known problems. It’s not a wholly incorrect argument, either. There are abstractions that we’ve built that solve a great deal of problems for us in advance. Databases are probably the best example: centralizing state behind a uniform interface, with solid guarantees about durability, atomicity, concurrency, etc. This is the type of abstraction that is invaluable to almost every piece of software.
But there are limits to this kind of thinking. There is an innumerable amount of other things that we keep trying to encapsulate to no end, that end up making the act of programming feel wasteful. The unfortunate part is that the high level systems we like to reason about are no less complex than the sum of their parts. Each layer of abstraction that we add does not actually save us from the maintenance of the lower layers, it just ignores them until they break (and they will almost certainly break).
When you deal only in these abstractions, you no longer set out to write a program. You set out to build an app, that will use a complicated frontend framework, that hooks into a specialized API layer. Your API layer interacts with dozens of services, some of which may even be written by you, if you’re lucky. A request fans out to dozens, to hundreds, each of which walks such a unique path within your cross-service call graph, that you set up tracing systems just to figure out what went where. You spend your days looking up constructs from your chosen frontend framework, which has 3 ‘CRITICAL’ vulnerabilities according to npm. But it’s not too bad, because fixing it only requires two breaking changes and adding twelve dependencies.
You read a StackOverflow post from 8 years ago on how you can wrangle your backend into working properly with this nice library. It’ll solve all your problems, thank goodness! The pinned response is only slightly condescending, and plus it only takes a couple of hours to configure your build system to get it all working. But of course, before you do that, you need to install a toolchain installer to manage your build system, and to do that, you need to configure your package manager to install the toolchain installer (because you should never build from source!).
Then, something breaks. Luckily, your logs are there to save the day(s)! After a week you figure out that a library that you transitively depend on has changed its behavior in a way that’s only partially backwards-compatible. You are one of the (unfortunate) 7 billion people that don’t immediately understand what this library does. But that’s ok, because your build system lets you pin its version to the last compatible version until you work up the courage to deal with it later. You hope later never comes.
I hope you can see my point. Programming often feels like learning about systems other people have built, and learning just enough about them to make what you want to do materialize. This separation is what makes people disillusioned with the practice; the constant dissonance between building up and breaking down abstractions that wind up out of your control.
I’m not arguing that our systems should never be complex; more capable software necessitates more complex systems. But the problem is that the way we adopt this complexity feels detached from the principles we’re taught from the beginning. The complexity feels like it’s something that grows beyond our reach, that we have to fight to manage and wrangle at each step, rather than something that we choose.
We spend time laboring over abstractions, documenting their behavior (or hoping the user figures it out), and cobbling them together to solve a problem. Over time, it becomes a burden; you no longer get hired because you know about computers, you get hired because you know the current most popular web framework, up until it’s deprecated. So then we try to build higher-level systems to manage the growing complexity beneath us; new languages, new build systems, new libraries. Each of these layers bundles components from the lower layers into something supposedly more manageable. And when it starts to become untenable, we go one layer higher.
This is why it’s everywhere. AI is great at taking these layers of complexity and managing them; summarizing the documentation, explaining under-documented dependencies, churning through logs. It’s an excellent tool for tackling complexity, and it feels good to use because it saves us all the time we would’ve spent managing it all ourselves. Having such an excellent tool at our disposal gives us the illusion that we’re able to do more, when in reality it’s just good at hiding all the bad things we’ve built our system upon.
But, as with any higher level abstraction, just because the complexity becomes easier to manage, doesn’t mean it goes away. When things break, it emerges once more and threatens to ruin the entire system. With its emergence, our high-level understanding is now threatened with all the unknowns of our underlying system. Modern programming practices seem to care little about technical debt in the face of velocity, but software as a whole suffers as a result. We end up thinking less about what our programs are actually meant to do, and what the act of programming is actually meant to create. We create software that’s slower, more resource-intensive. These things compound across the ecosystem, and suddenly the “UNIX philosophy” starts to choke on hardware that could easily send a man to the moon.
I have no doubt that AI can be used to solve so many cool problems in Computer Science. I like using it, and use it as much as I can, in as many creative ways as I can to manage the complexity of the systems I interact with daily. But it’s increasingly apparent to me that the more we build towards encapsulating all these bad practices behind abstractions, the further we move away from actually making better software. And in doing so, the further we move away from enjoying writing software altogether. I’m not upset because it’s everywhere, I’d just prefer a world where we didn’t require such advanced tools to manage the problems we’ve created for ourselves.