Write One to Throw Away?
If you're in the software industry for long enough, you'll hear this advice eventually. There's an infamous Catch-22 to writing code:
You don't deeply understand the problem (or its solution space) until you've written a solution.
The first solution you write will have none of that hindsight to help you.
So it naturally shakes out that you have to write it at least one time before you can write it well, unless you're stricken with exceptional luck. And the minimum number of attempts you will need is two: one to throw away, and a second attempt to keep.
It's just math. It's just logic. Write one to throw away. It's got the world's easiest proof. You'd have to be some kind of idiot to argue with it!
Okay, hear me out...
As you work on bigger and older projects, you will continuously be confronted with a real-world reality: that requirements are an input that never stops changing. You can make the right tool for the job today, but the job will change tomorrow. Is your pride and joy still the right tool?
If you're like most developers, your first stage of grief will be denial. Surely, if we just anticipate all the futures that could possibly happen, we can write code that's ready to be extended in any possible direction later! We're basically wizards, after all - this feels like it should work.
So you try it. You briefly feel safe in the corrosive sandstorm of time. Your code feels future proof, right up until the future arrives with a demand you didn't anticipate, which is actually so much harder to write thanks to your premature abstractions. Welcome to the anger stage. The YAGNI acronym (you ain't gonna need it) finally registers in your brain for what it is - a bitter pill, hard-won but true.
But we're wizards! We bargain with our interpreters and parsers and borrow checkers. Surely we can make our software immortal with the right burnt offerings. We can use TDD! Oops, now our tests are their own giant maintenance burden locking us into inflexible implementation decisions. Static analysis and refactoring tooling! Huh, well that made life support easier, but couldn't fix fundamental problems of approach, architecture and design (many of which only came into existence when the requirements changed).
As the sun rises and sets on entire ISAs, the cold gloom eventually sets in. There is no such thing as immortal software. Even the software that appears immortal is usually a vortex of continuous human labor and editing. The Linux kernel is constantly dying by pieces and being reborn in equal or greater measure - it feels great to get a patch merged, but your name might not be in the git blame at all in 2 years time.
I want to talk about what happens when your head suddenly jumps up in astonished clarity and you finally accept and embrace that fact: holy shit, there is no immortal software!
Silicon is sand
... and we're in the mandala business, baby.
I advocate that you write every copy to be thrown away. Every single one. I'm not kidding.
Maybe it'll be good enough (read adequacy, not perfection) that you never end up needing to replace your code in practice. Maybe you'll replace it every couple years as your traffic scales. But the only sure thing in life is that your code will have an expiration date, and every choice you make in acknowledgement of that mortality will make your life better.
People are often hesitant to throw out working code because it represents years of accumulated knowledge in real-world use. You'd have to be a fool to waste that knowledge, right? Okay. Do your comments actually instruct the reader about these lessons? Does secondary documentation explain why decisions were made, not just what those decisions were? Are you linking to an issue tracker (that's still accessible to your team)? If you're not answering yes to these type of questions, you have no knowledge in your code. It is a black hole that consumed and irreparably transformed knowledge for ten years. It is one of the worst liabilities you could possibly have. Don't be proud of that ship! You'll have nowhere to go when it sinks, and you'll go down with it.
When you write code with the future rewriter - not merely maintainer - in mind, you'll find it doesn't need to be replaced as often. That sounds ironic, and it is, but it's also true. Your code will be educational enough for onboarding new people (who would rewrite what they don't understand anyways). It will document its own assumptions (so you can tell when you need a full rewrite, or just something partial that feels more like a modification). It will provide a more useful guiding light for component size than any "do one thing well" handwave. And when the day finally comes, when a rewrite is truly necessary, you'll have all the knowledge you need to do it. In the meantime, you've given yourself permission to shit out something sloppy that might never need replacing, but will teach you a lot about the problem domain.
This is independent of things like test suite methodology, but it does provide a useful seive for thinking about which tests you do and don't want. The right tests will improve your mobility! The wrong tests will set your feet in cement. "Does this make a rewrite easier?" is a very good, very concrete heuristic for telling the two apart.
Sorry for long-posting, btw. I used this space to work through some hazy ideas and sharpen them for myself, particularly because I'm looking at getting into language design and implementation in the near future. Maybe at some future date, I'll rewrite it shorter and clearer.
TL,DR:
Every LOC you write will probably eventually be disposed or replaced. Optimize for that, and achieve Zen.












