Holding a Program in One's Head (2007)
paulgraham.com
paulgraham.com
There is certainly some truth to this. On the other hand, it's possible to become blinded to defects in code you've written yourself. You see what you intended for the code to do rather than what it actually does. Reading someone else's code, it can be easier to see what's really going on, since you just see what's there.
Open any textbook (even a fourth edition of a famous one, written by an expert) and you’ll find countless typos. In fact, in a certain sense, the more expert you are the less suitable you are as a proof reader for such books.
This isn't quite true, especially concerning distributed systems. It's relatively common for a software system to be broken by design. It's not that the developer didn't know how to use the programming language to get the computer to do what they want. It's that what the developer wanted reflects a poor model of the world, a logical inconsistency, or just a behavior which is confusing to users.
The obvious exception are recursive number-fiddling algos which would spam gigabytes of output due to big N.
This way I can just read assumptions and see branches taken and what’s wrong as if it was written in plain text.
When I see klocs without a single log statement, to me it’s readonly and not worth touching. If you’re stuck with a bug, log everything and you’ll see it right there.
Ad hominem arguments don't land so great, do they?
I was responding to your blanket claim that server-side is misguided in general.
I have no idea what you consider complex, but most people pushing toward server side UI are not advocating that it's a one-stop solution, just that it simplifies a large majority of situations that many of us are in which is building CRUD apps. You can even get pretty complex, like say an email client though at that point we're in a grey area where you could kind of go either way. If we're talking something like building PhotoShop in-browser or even a calendar or gantt chart (which I have worked on) then, no, I would not personally advocate server side and instead use a good client-side view library.
The Elixir/Erlang comment was that it makes server-side even easier as you can hop into running production systems and debug them.
Then a review becomes something like "the claim was made this function does this, does it look like the function does what it says it does?" If you can understand it in context, then you've added trust and knowledge of how it works.
However it seems to often be the case a review turns into "I like this word better than that word" so back to the explaining it point, it becomes a bit of a self review with the hope it might be helpful to somebody else in the future.
My advice on this would be: never be afraid, and even force yourself to, follow the chain of how a certain function is implemented in third-party libraries you are using. Set up your IDE to be able to click into functions, make that muscle memory, and perhaps even have a dedicated project you can bring up for your site-packages or node_modules. Don't just rely on documentation, see how things actually work a few levels deep. You'll gain a deeper understanding of the code you're using, as well as incredible practice for when you need to do this on an existing first-party codebase.
Oh, and if you can, get one or more large 4k monitors, and split your IDE into 4 quadrants! It's certainly possible to hold a codebase in your head on a small laptop screen, but being able to see the code you're working on alongside its dependencies and dependents makes this far easier!
I'm not sure about the advantage to seeing all the code at once for understanding it. Surely for refactoring or massive editing, but to encode it into your brain, I feel the crucial thing is more abstract than actually seeing the code.
I personally find the large screen is something that's more useful when I am moving between different low-concentration tasks (confluence, slack, teams, emacs, whatever) but for deep problem solving/thinking, just 1 window I can see everything completely with a full-screen emacs is more conducive. I assume other editors than emacs would have this same facility. Extremely rapid answering of different questions about the code via LSP type facilities is more key than seeing a bunch of text at once, so I can follow the flow of my thoughts. Sometimes, when I'm trying to make sure that 2 or 3 functions have the same flow/logic (yeah, haven't found the abstraction to replace them with) I'll want a big side-by-side comparison of 2 or 3 places at once.
This ability to follow into third party code you use was one of the initial attractions of Gentoo - you can build all from source, keep source and symbols around, and edit/trace the code for anything on the system. Reading a lot of code I think makes my own code better.
I like to explain this as “hide the bad parts behind a good API”. Anything interesting is going to require “bad parts”, which just means the low-level, difficult work. From there, compose up and up until a high level orchestration is achieved. It works so much better than the bad parts being distributed everywhere! That’s what you’d also call a “leaky abstraction”
In my view, the purpose of abstraction is to compress the concepts of your application into digestible and manueverable chunks, and DRY is just a heuristic for beginners to follow to help point to where appropriate abstraction boundaries may be.
I hope the various theories behind what constitutes good code will make their way out of scattered blog posts and into CSE curriculum.
I actually took a general Software Development course when I was in school that sort of touched on things like Linux, source control, debugging, and other things I now forget— it was an elective though. It was neat, the teacher even introduced us to Ruby, which was a cool experience for a bunch of dumb kids who thought the whole world ran on Java or C++ :) I liked it so much I went on to implement Ruby-isms in C++ for my other classes, and use those to orchestrate solutions. Completely dumb in retrospect, but I thought it was so cool at the time. And hey I guess that’s a great example of learning early on how to “hide the bad parts!”
The amount of times I've had to dig through 6 levels of abstraction to find what error gets returned in some edge case so I can catch and handle it gave me PTSD and gave me not invented here syndrome.
As well, the division of a project into layers where each layer has a manageable amount of concepts, 5-7 for normal layers, becomes much easier for people to learn and use correctly. If I have to keep 12 things in mind while using a layer, it's going to be a lot harder to get correct.
DRY is for improving communication with your future self.
This doesn't work for the "bad things" I know of. All of the low-level bad parts which are truly bad/mucky, are bad because they can't be entirely hidden. They are leaky abstractions and by that fact, they ... leak. They impact every part of the system so they are "distributed everywhere" in the sense that the maintainer of a large program has to consider them everywhere even if they only directly show up in a few places.
Just as an example, programming languages are abstractions over the complex structure of "raw" memory. Pythons hide this abstraction more than for example c but once you reach a certain scale the use of python, you have to consider the details of its use of memory. And that's memory in general, which people work at to make reasonably regular as well as fast.
That's not saying you can't have an API that makes the low-level problems less.
https://www.goodreads.com/book/show/39996759-a-philosophy-of...
and I find that the mechanism of "Literate Programming":
https://literateprogramming.com/
is a useful one for doing this since it allows one to write about both how the low level details are implements in a function, _and_ how the written function is used in a way which allows the twain to support each other.
Bonus points if you can distinguish between essential state and circumstantial state.
Break it up into a few or several smaller programs that interact through clean interfaces. Then you can keep one smaller, simpler program in your head at a time, then integrate them at the end once all the smaller programs are working.
Sometimes it is one or more programs writing to a queue or topic, and other programs reading from that topic.
Or programs writing to and others reading from a Unix pipe.
Or programs talking to each other using HTTP.
Or Erlang processes communicating concurrently on one machine or across a network.
Or different programs sharing one database.
Or many objects communicating by passing messages in a small talk program.
There are many ways to encapsulate programs and have them interact.
write(message), read() -> message
> Or programs talking to each other using HTTP.
request() -> response
> Or Erlang processes communicating concurrently on one machine or across a network.
sendMess(message), waitMess() -> message
> Or different programs sharing one database.
execute(query) -> response
...
I'm a mathematician at heart so I'm staying away from category theory as long as possible.
That's not great.
Unfortunately been dropped into that situation. The system was constantly broken due to missing of malformated arguments. Nobody dared to change anything.
Same applies to prematurely microservicing a monolith. At least there folks seem to be sensible enough to use protobufs or json, instead of loosely typed strings.
Of course in a corporate setting it is much harder to do as you have code written in multiple languages using multiple patterns stored in different places, so often there is not underlying architectural thread to hold it all together.
Yes, this was in the microservices-heyday.
Those abstractions don't just poof into existence after the project is complete, though. You have to design them into the system and communicate them to the people who need them.
The abstractions also need to be precise and non-leaky enough to be useful. One of the most dangerous talents in software is the ability to create the illusion of tractability by using vague language. You can create an absolute mess and then describe it to management in a way that makes it sound well-understood. This is the most lucrative and destructive skill a consultant can have.
7. Don't have multiple people editing the same piece of code.
You never understand other people's code as well as your own. No matter
how thoroughly you've read it, you've only read it, not written it. So
if a piece of code is written by multiple authors, none of them understand
it as well as a single author would.
On some level it's true but it is also true that most of the world's code is in some sort of maintenance mode and the original developer is not always available. When I work on code that I think would be difficult to maintain I write a lot of comments to explain invariants that should be followed or anything strange. (Hmm, there are three different kinds of "row" talked about in this source file)If you have a front end-back end system and you want to do something simple like add a field to a form there's a strong case for a single developer or maybe paired developers to make all the changes to make that change happens as opposed to assign two developers (not paired) to communicate with each other about doing the task, or worse yet, assign two teams. You might have had two people build out the front-end and back-end systems but for sustainable maintenance one person should be able to ship a feature.
But there’s no time in the schedule for that? Either that’s not true (the long way round is usually the shortest way home); or it’s time to find another project. And if you’re the sort of person who has the skills Graham describes, that shouldn’t be hard.
However I have one quibble. Almost invariably when I explore other people's code, there's a lot of these "wtf" moments and some times they end with "oooh that makes sense" but honestly most of the time they end with "ugh, this is stupid".
For example i was tasked with factoring out Newtonsoft.Json in favor of System.Text.Json in an Azure function. I looked through the code and it was some of the worst shit I've ever seen. And completely untested, there were two test classes with a few test functions but they literally tested nothing. The only way you could have failed those tests would be by adding a `throw new Exception()` as the first line of the function the test was calling. Seriously. The rest was a for each loop iterating over a list parameter and the test was passing an empty list.
The core of the whole thing was this big chain of functions where over a dozen different parameters were passed down through about two dozen different methods, some weren't even used, some were repeatedly serialized to json, passed as a string to the next method, deserialized and passed as an object to the next method etc. There was this whole complicated AsyncEnumerable setup that served no purpose at all except complicating the code. There was a lot of other stupid stuff. I really don't think my description adequately conveys how bad this code was.
It was fucking atrocious. A coworker did some pair programming with me and suggested asking the author about it, I said I didn't want to talk to him because I didn't know what to say. I don't think I could have had that conversation without just completely shitting all over his entire project. There weren't any questions to ask, there was no "maybe I just don't understand the reasoning". I understood the code perfectly and it was shit. Plain and simple.
So I did what I tend to do in these situations, which is the subject of my quibble - I rewrote it. Instead of trying to figure out how to do what I needed to do in the context of this complete mess, I wrote some tests to establish the current functionality (surprise, it uncovered multiple glaring bugs - not sure how nobody noticed it wasn't even working properly for months) and then I just deleted all the trash code and wrote it properly. It wasn't a very complicated program, it literally just gets some data from a few API endpoints, massages it a bit and sends it off to an event hub as messages.
So my quibble is that a lot of people don't appreciate this approach. They call it scope creep, I get some task that should be fairly small but it leads me down a rabbit hole where I end up rewriting or refactoring large chunks of code in addition to what I'm supposed to do.
I think it's a nuanced topic, I think my approach is appropriate for some situations and not for others. For example if I'm working on an app that only needs to exist for another 6 months it's reasonable to minimize the effort spent on it. Doesn't make sense to refactor stuff. In the case I just described I think the rewrite was appropriate. My team agree with me on that, though I'm not sure everyone would. And I'm having trouble drawing that line, when should I fix stuff and when should I work around it? I love great code and I hate bad code, it honestly really bothers me when I have to work with some moron's spaghetti. So I think I'm pretty biased towards fixing stuff like that when I see it. I'm happy to be in a team that appreciates this side of me but I'm worried about when I inevitably end up in one that doesn't.
A properly architected code base makes it easy to make the kind of changes you want to make. A badly architected code base (as you describe) makes it nearly impossible. I've found it's usually by far the quickest thing to fix the architecture and then implement the new feature sanely. (And no, you don't ask permission; you just make everything clean. As I noted above, if your boss doesn't like this kind of thinking, you need a new boss.)
Of course, I'm one of those guys Graham mentions who works by himself most of the time.....YMMV.
So I frequently had to learn a new js framework while dealing with shitty, unreviewed and untested code that barely worked or just kind of seemed to work, while under time pressure.
In that environment, my disdain for bad code really just didn't work out. Though I feel like I was a bit unfairly treated at times as well.
There was this one app that had an integration against a third party API. They had used a library to interact with the API, this library was purchased for a fairly significant amount of money from some university and it was complete garbage. All it did was get data twice a day and store it in a db, then the CMS had a daily scheduled job that would get the data from that db and build the content in the actual CMS db. It was like 30k loc, a lot of which was dead and the rest of it was shitty and redundant manual xml parsing and completely unnecessary db interaction. Not to mention the (probably minor) cost of running this pointless db.
So there's a problem with it and I spend days bughunting, I can't figure out what's wrong. I try telling the team that I think I can build a better solution from scratch faster than I can fix this bug. No bueno. So over the weekend, on my own time, (really just a few hours) I throw together a quick POC. I don't need the intermediate DB, the CMS can just get the data from the API once a day during the scheduled job. So 90% of the work is just making DTOs to deserialize the xml into, using whatever xml serialization library i found.
I finish it, hook it up, it's not totally done but it pretty much works already. On Monday I tell the team what I've done and just get completely shut down. They don't even want to see it, just nope.
So that code is gone and they're still maintaining that piece of shit library for no reason. Oh well, luckily it's not my problem any more.
Sorry about the rant, and thanks for the support. It's nice to know I'm not alone in seeing the value of quality.
For example, consider this analogy: when we make a car, we have one wheel and a multitude of parameters that change over time. The wheel is a derivative of these parameters. In car manufacturing, time is virtually unlimited for improving the quality of the wheel. But when we develop software, we always face time constraints and an enormous set of parameters. Thousands of car engineers spend decades improving a single wheel with a limited number of parameters, while a single programmer (or a couple of programmers) have only a short amount of time to release software with a randomly variable number of parameters (a service depends on Auth0 and its parameters change). As a result the programmer cannot be certain that all parameters have been correctly handled. This is why the programming process is similar to learning, and this is where LLMs are the best.
Just to elaborate on "start with a simpler subcase": you can even start with a single example and do what your code will be supposed to be doing automatically by hand. After your draft architecture stands, you can even hardwire what each component does for that one example to see if things fit together before implementing the general method for each component, which provides an opportunity for early integration testing that is priceless.
The current essay recommends building bottom-up primitives that serve as primitives or languages (DSLs) for the next levels. This is true, but you can do this upside down. For example, you can write code that looks like pseudo-code because implementations of functions you call do not (or not yet) exist. The code directly reflects how you think, and you worry about the implementation later. A special instance of that is API design, where you design interfaces without implementing them yet; you can then write client code and judge if the hypothetical API would be flexible enough, be easy enough to use, and be feature complete and orthogonal. When designing an architecture in a team with multiple people, you can use paper cards with an example piece of data that you pass from person to person, with each person "performing" a component. They can then check that they have what they need to carry our their task (otherwise there may be bad surprises when integrating components later).
I found that some people are more leaning "top down" and others more "bottom up"; I like to mix both styles, with system architectures designed top down and the core algortihms inside their components often being designed bottom-up, sometimes also top-down.
Ironically, looking only at the headline, one could say that abstraction enables one to solve problems without getting the whole problem in one's head, or problems bigger than what anyone can hold in their head, at least not at the same time, which kind of is the whole point made by SICP.
> Thanks to Sam Altman, David Greenspan, Aaron Iba, Jessica Livingston, Robert Morris, Peter Norvig, Lisa Randall, Emmett Shear, Sergei Tsarev, and Stephen Wolfram for reading drafts of this.
(I don't recognize some of the others.)
"Keep the whole program in your head?"
Cool advice for small greenfield projects that you can sell to unsophisticated companies and then move on. Interesting that it comes from an investing firm that makes this precise activity it's core business and reviewed by people who are in a position to directly benefit from this.
This is all terrible advice if you want to actually contribute to or work on anything significant in your life.
I used some tiny devices (e.g. Casio PB80 http://oldcomputermuseum.com/casio_pb80.html) as a kid to write small programs on in BASIC. It has only a single line of display and you had to sort of remember where things were to make your GOTOs work properly etc. This means that most of the program had to be in your head. You couldn't easily jump between things and see the whole thing as a single screen of text. I also remember reading that people like Ken Thompson wrote significant parts of the original UNIX on a line editor (ed). That would also mean that most of it had to be in his head in a semi parsed format.
Sure, with modern editors and IDEs, you can outsource that to the computer and focus on the more valuable stuff but I still wonder how much the grunt work of remembering things in detail is under appreciated in creative work.
(I'm sure there are exceptions, but in the set of languages that enable succinctness, the subset that force succinctness is surely small.)
What does "succinct" mean? Well, it means terse but not too terse. What does "readable" mean? It means I can easily understand it.
How is this helpful?
It's possible to write code so succinct that making it more verbose would make it more understandable.
But overly verbose code is far, far more common. And having a naturally succinct language does not stop you from keeping your code verbose. Just gives you more ways to make your code succinct that aren't possible in non-succinct languages.
(Not this is orthogonal to weaker or stronger compile time type systems. Type systems that catch more errors at compile time are very useful. But some strongly compile-time typed languages are still quite succinct.)
https://paulgraham.com/power.html
> I think that the main reason we take the trouble to develop high-level languages is to get leverage, so that we can say (and more importantly, think) in 10 lines of a high-level language what would require 1000 lines of machine language.
...
> 5. Write rereadable code. All programmers know it's good to write readable code. [...] If you're writing for other people, you may not want to make code too dense. Some parts of a program may be easiest to read if you spread things out [...] Whereas if you're writing code to make it easy to reload into your head, it may be best to go for brevity.
Not only is that often false, but there are times when you can understand another author's code better than that author does. Like to see through it and why it cannot possibly work, while they are laboring toward that.
I remember the CTO of a big American bank telling me they didn't want to develop any software in-house. Their plan was to buy everything in. At the time I thought it was rather strange but having dealt with a lot of enterprise software since then I can see why.
It's a very pragmatic choice for the majority of business use cases. It should be more common imo, but some execs enjoy the empire building aspect and underestimate TCO when looking at cheap offshore dev resources.
And if you do it wrong (overengineering), you would need to learn not just one, but as many programming languages as there are layers...
I imagine a future world where 100% of coding and debugging is done by prompt not by editing code.
Unless, you know, it's automatically tested to hell and back.