
Generative AI
There has been a lot in tech news and opinion recently about what generative AI will or won’t do, such as take away jobs from programmers and testers. I’ve had a long enough career in software to be able to put generative AI in a bigger context, which I think helps to understand some of the nuances beneath the headlines.

In the beginning…
The first part of this article is a trip down memory lane, starting with my first experience of programming.
I started programming on a Sinclair ZX81 and then a ZX Spectrum. This was mostly in BASIC, with a little Z80A assembler, and it was as a hobby rather than as a job. I still remember having to put wait instructions in some assembler code, because it was updating the screen too quickly. After BASIC this was like a different world.
One of the things that you’re very aware of when programming in assembler on a small chip like a Z80A is how few registers you have to play with. These are the equivalent of local variables, and the Z80A’s hardware means you can use no more than 8, with each storing a number in the range 0-255. If you want to store more than 8 pieces of working data, you need to manage overflowing to somewhere like memory by yourself.
Programming in assembler is blazingly fast, but it comes at a cost – how much you must keep track of by yourself, the tight restrictions on local variables, and how little effect each operation has on the world. It’s like you’re trying to lift a chair by moving each of its atoms one by one, very quickly.
However, even though I was so close to bare metal, there were still things I didn’t have to worry about. I was using an assembler, so I could write my code using commands such as RET (meaning return), rather than having to remember the equivalent number (C9 in this case) from the chip’s machine code. I had to think about performance a little, but not to the extent of worrying about the size of the chip’s instruction cache, or how its cycle of fetching and decoding instructions was affected by branches in the code, etc. People who wrote the assembler had these kinds of worries, but I didn’t as an application programmer.
I did have to worry about edge cases and errors in my code. What if the user tried entering an invalid value? What should happen when the cursor got to the edge of the screen?
The compiler will C you now
My first job as a professional programmer was in C. Compared to assembler, these were giddy heights of abstraction. There were built-in instructions that could do things like compare a pair of strings in one go – now we’re moving big molecules at a time and not just atoms. You could have as many local variables as you wanted. You could separate your code into a public interface and private implementation. The operating system provided luxuries like virtual memory.
However, you still had to allocate and free memory yourself, which took a lot of discipline if you wanted to avoid memory leaks (and the associated crashes at unpredictable times). Things like pointers were a double-edged sword – you could create all kinds of amazing data structures and use hairy amounts of indirection if you wanted to, but you had to keep track of what they pointed to yourself. If you didn’t, then you could do things like run off the end of a string and read or write random bits of memory, with more crashes at unpredictable times.
The programming was all procedural, rather than object-oriented or functional. You had to worry about inconsistencies between different platforms that your code tried to run on, as the code was compiled down to a native executable. For instance, freshly allocated memory on Solaris was automatically initialised to all 0s, but on other flavours of UNIX it still held whatever it had the last time something wrote to it. If your code had forgotten to initialise pointers to NULL, this would be masked on Solaris, but it would produce unpredictable crashes on other flavours of UNIX.
Now there were paying customers involved, we had to thrash out the details of what was required. The request for new or changed code started vague and brief (e.g. add a friends and family discount to a billing system), but then need working on by skilled people putting in a decent amount of effort. Things needed to be broken down several levels in terms of detail, so that the request was at least a paragraph – often several. All the relevant people needed to agree on what the request was, and how it fitted in with the rest of the system.
We had to worry about errors and edge cases – what happens if this number is too large, or the file can’t be opened? We had to worry about performance a bit – a lot in the most critical parts of the system – but never to the extent that we needed to second guess what the compiler was doing on our behalf when it turned our C into machine code. We also had to worry about correctness, using a debugger and automated tests to gain confidence that the code behaved as we (and the customer) expected.
Objects
After C I skipped a generation and moved straight to C#. This was even more luxurious – for example, memory just magically allocated and freed itself. You had all the goodies of object orientation such as inheritance, virtual methods, exceptions and so on. Your code could come very close to expressing stuff that mattered to people in the real world, using language that wasn’t too hard to understand. The virtual machine took care of differences between platforms, at least most of the time, so C# could run on a mobile phone, a large server or your laptop.
It was interesting programming in C# as the language itself changed. It became more powerful (with things like LINQ), and reduced how much tedious typing and reading it imposed on you (with things like lambda expressions, anonymous types etc.) These further lifted the level of abstraction at which you could work, and distilled the code down to solving the problem at hand rather than diluting its meaning with noisy syntax and busy-work.
However, we still had to worry about performance a little, plus edge cases and errors. We still needed confidence in the code’s correctness, and so used tests and a debugger. We still had to turn a vague and brief request into enough detail, that was agreed on, that it could be implemented.
Data
As well as software engineering, I’ve also done some data engineering. This was partly in SQL, and partly using ETL tools.
Something that not everyone who writes SQL is aware of is the execution plan prepared by the query optimiser. SQL isn’t directly understood by the database, but is instead used to create a lower-level set of operations. This set of lower-level operations is called the execution plan, and the thing that creates it is the query optimiser. For instance, if the query joins two tables, which one should be the driving table? If the query is trying to return only some rows, is there an index that would help (and, if there are several candidate indexes, which one is best)? You don’t write the execution plan directly, but instead write at a higher level in SQL.
The execution plan can vary based on what indexes are present, and how many rows are in the tables involved in the query. This can add an element of non-determinism to results, i.e. queries can take longer than expected.
Even though the query optimiser can help you, it’s not magic. You still must worry about performance, including how the database is designed. However, there are depths to performance that most people don’t worry about most of the time, such as tuning parameters on the database server.
We cared about correctness, and so we tested the SQL. We had to cope with edge cases, for instance if a join should be an inner or outer join depending on how data could be null or not, and how we wanted that to affect the result.
When I worked with ETL tools, SQL disappeared into the background most (but not all) of the time. There were standard building blocks for reading, writing and manipulating data, that needed connecting together and configuring. However, even then, understanding what the code needed to do took some effort and time. There were still errors and edge cases to deal with, and correctness to check with tests. We still had to worry about performance, to the extent that we made sure that we did as much as possible with some data before getting rid of it, so we wouldn’t have to read it more often than necessary.
Common themes
I hope that you can see some common themes across my career. The abstraction level generally rose over time, away from technical details and towards the world of people. The rising level was due to a combination of the tools getting more powerful, and also solving problems only once and putting that solution into a reusable bit of code. Now I wouldn’t have to e.g. implement the details of parsing an HTTP request each time, but I could instead rely on an HTTP library that handled that for me.
No matter what level I was working at, there was always something below me taking away details I didn’t care about – an assembler, compiler, operating system, virtual machine, query optimiser, ETL runtime etc. I didn’t think that I was cheating by using them, and most of the time I could take them for granted.
I had to care about correctness, edge cases and errors, which required thought and tools like debuggers and tests. Thrashing out what should be built took thought and conversations. These were (apart from the assembler code) long-lived systems that several people worked on over several years. So, the systems grew and changed over time, such that they could continue to do what they already could, but also learned new tricks. Performance was an issue some of the time, so I would occasionally need to dig down one level in the stack of abstraction levels (by knowing how it worked).
Generally, I was solving problems and expressing myself in code.
Generative AI
Finally, with all that context established, what about generative AI? Part of the problem is that there is a lot of noise mixed in with the signal. There are claims that it will take away jobs, do everything including making a nice cup of tea, or not amount to much at all. Some of this is people trying to sell things, or getting caught up in the excitement of something new, or scared for their future. All of these are valid, but can make it harder to see the core of things.
Claims that everyone will soon be able to program, and so we won’t need specialist programmers any more have been made at least since the dawn of COBOL in 1959. I think that these claims have been partially true, and will remain partially true for things like generative AI.
The true part is that a bigger group of people will be able to do things that previously used to be restricted to a smaller group of people with specialist skills. For instance, I can write programs without knowing the details of memory management, task scheduling, communications and so on, because these are all handled by the operating system. This was, in turn, written by people who specialise in writing this kind of low-level code. Previously, every application programmer had to worry about all this detail as well as the specific task their code was trying to achieve. This has broadened the group of people who can program.
Similarly, people can now get a computer to add a list of numbers quickly and reliably in a spreadsheet, rather than having to know how to punch cards and feed them into a mainframe. Again, the group of people who can get a computer to do something has grown, as the tools have changed and got more powerful.
The false part is “… so we won’t need specialist programmers”. Programmers act as a bridge between the rigid and ordered world of computers and the messy and complex world of people. We must work out how to carve out a slice of the people world that is consistent and complete enough to make sense, work out its rules with all their exceptions and special cases, and then express all that in a way that the computer will understand. We must work how to get the program to be understandable by people who didn’t write it, so they can feel comfortable using it.
Note that a lot of the activity described in the previous paragraph isn’t just typing. Much of the hard work can be done away from a computer, e.g. around a whiteboard with other people. The specifics of how the computer is instructed aren’t all that important, no matter how excited some people get about the latest shiny tool.
This brings me onto the next point, which is the trend of abstraction levels to rise over time, away from technical details and towards people. One way of looking at things is that generative AI is a continuation of that trend.
Quickly following that is the worry that generative AI is dumbing down programmers so that they will have brittle and shallow knowledge and be reliant on the tool. I think it’s reasonable to worry about how resilient your skills and knowledge are, and how much you rely on tools. Can you dig down into the lower levels of abstraction on the rare occasions you need to, even if you’re not comfortable there? Can you tell when your tools have made their occasional mistakes? Can you tell the limitations of different tools, and so know which one is the right one for a given job?
However, when was the last time you spent hours hand-optimising the machine code that your program produced? Isn’t this what real programmers can do and have to do? A few decades ago, yes it was. In some fields, such as the extreme end of embedded programming, it probably still is sometimes. But this skill has shrunk to a tiny corner of the set of programmers, while the set overall has grown enormously.
There are a few issues that relate to professional programming much more than they do to demoes or personal projects. If the system you’re working on is valuable to someone (usually, the person paying your bills) then it’s good to have confidence that it works as expected. This is where generative AI can look a bit shaky. Unless you’re in control of the model, you don’t know that the results you get today will be the same as the results you got yesterday for the same inputs.
Also, it’s hard to know why it produces the results it does. This is a bigger issue than just a single result for a single input. Knowing why a system has worked in a particular way can help you extrapolate, so you can predict how it will work on different inputs in the future.
Whether generative AI is producing something that you put directly into the hands of users, or is e.g. producing part of the source code that is a few steps away from users, having confidence in what you’ve put your name too seems to make sense to me.
All the systems I’ve worked on have grown over time. We were able to take its specification (in the form of source code), modify that specification, and have confidence that its past behaviour would continue apart from where we had modified things.
As systems grow and get more complex, it gets harder to reach that confidence and the maximum confidence you can reach might shrink as the complexity increases. However, these effects tend to start a lot further out than I think will happen with generative AI. Changing a prompt in a small way seems to have a less-than-predictable change in the result you get. Prompt engineering seems to be as much art as science or engineering. Maybe it will get more predictable over time, but I don’t know.
Summing up
There are lots of common themes that have existed in computing for a while, and seem to continue with generative AI. These themes can affect people deeply, such as making people fear that their skills will become obsolete. If your livelihood and self-image are tied up in those skills, this can be really unsettling.
There are limitations to any tool, including generative AI, and it’s good to know them despite what hype-mongers are saying. Just as COBOL and C were once the hot technology of the future, but are now buried several strata deep in tech archaeology terms, one day generative AI will be out-dated and embarrassing to people peddling the next latest thing. Until then, it’s worth thinking about what it can and can’t do, and so how much it can (or can’t) help you.