348 comments

[ 4.6 ms ] story [ 280 ms ] thread
I would argue that correct is more important than simple.

Consider timezones: it's simpler to pretend there's 24 time zones, one for each hour. But the correct assertion is there's 37 time zones (as of this writing). So, the simple solution results in a third of your potential user base having issues.

Other issues to pick: accessibility, cross-browser compatibility, legacy device compatibility... the list goes on.

Obviously to solve the problem, a certain amount of correctness is implied. But once that threshold is reached, simplicity is a better use of your time. At least, that's the thesis of the article.
True.

The problem is always, how to get enough correctness that your customers are happy, but don't spent too much time on it so that rivals won't overtake you.

My read of this that simple means not necessarily handling all possible edge cases yet having enough of a skeleton in place that the core functionality is there. Correct is extending (or replacing) that skeleton with coverage for all cases.
Edge cases? You mean we can't test for leap year by simply checking if the year is a multiple of four?
No that’s a silly example.

I meant more for dealing with edge cases like an external API invocation failing. A final “correct” implementation would need to handle failures but an initial simple one would only handle success.

Agreed. Software that does what it's supposed to is better than software that is simple (or fast), but buggy.
Which is why everyone uses OpenBSD instead of making a tradeoff with a different OS that isn't designed as securely.
Users go for usability features first, with an OS that is good enough. Good looks second. They can tolerate a few weirdness and quirks as long as they can get the job done without cursing the thing. They will even tolerate non critical crashes sometimes (no or little data loss).

OpenBSD is not bug free at all, it is just security oriented in the implementation.

Windows got traction because it has even better hardware support, a bunch of backroom OEM deals and nice UI features (at the time of 95), then went far on software availability.

Correctness (and any other kind of change in behaviour) is easier to achieve if things are simple.

Changes are not necessarily easy or even possible to make safely if things are correct but not simple.

"Simple" is a proxy for "can be changed safely" and so IMO is the most important quality to have.

I think you do whatever it takes to achieve correctness.

If simplicity helps achieve correctness, then great.

But correctness is not always simple.

Most people think there is a leap year every four years. They are wrong.

The simple thing to do then is to call an `isLeapYear` function that abstracts any complexity.

Abstraction allows you to hide complexity and make it a simple, reusable part again.

But I could write a simpler (incorrect) leap year function than yours!
You missed the point of the article if you interpreted it as correctness being intentionally sacrificed for simplicity.

Plus, a complete leap year implementation is already what I would consider simple and most standard libraries already have an implementation for it you can use directly.

(comment deleted)
I know a couple of guys that would probably implement isLeap by checking that the lower two bits of the year are not set, then boast about how fast it was, and finally, when told that the logic was incorrect, push back because the additional checks would make it "slower".

In their minds, when things stop being simple, fast trumps correct.

I don't think simple means discard if complex. It means using the simplest and most obvious tools (algorithms, libraries, &c) possible in order to achieve a correct enough software. So it does not mean discarding intricacies of timekeeping, but implementing it with the clearest, simplest use of abstractions and methods possible.
My calculator watch worked perfectly for 50 years on that "wrong" assumption.
One nuance is that some parts of correctness may be negotiable.

If I'm writing a file backup program, it absolutely must back up all the files without leaving any out or corrupting the data.

But let's say it has a feature that prints progress indicator percentages on the command line, ranging from 0% to 100%. Maybe under certain circumstances (like files added to a directory after the backup starts), it prints 102%. It's not what I had in mind, nor is it something I'd call correct. But if fixing it complicates the code a lot, maybe leaving it that way is the better choice.

(This is a bit of a contrived example because you could just clip the value at 100, but you get the idea.)

Every piece of software that has bugs could be considered not correct, which is the vast majority of it.
Depends on the definition of bug.
All general rules like this are terrible if you apply then blindly, but can still be useful if you apply them loosely.

In your example, I would take simple to mean "only use UTC". As soon as you need timezones I would say you've moved into correctness territory and need to do them all properly (you would use a good library, of course).

The point is that it should be as simple as possible to solve the problem but no simpler.
This generic expression indicates why "correct" comes first. "Simple" has it as a dependency. ;)
It's order of importance though rather than an expression of dependencies and I'd definitely err on wanting my solution to be as simple as possible over merely correct. The idea being that whilst you are solving a problem you are always striving to solve it simply and only add complexity as needed.
But would it be better to account for non-end of year leap second additions vs something simplier that just ignores them but is wrong once every... decade?
Depends on the software.
I think it's more in the spirit of the article to say, forget timezones, use UTC millis everywhere. If the server doesn't speak in timezones, then you eliminated all bugs where the server mishandles timezones.
In my experience, it's been easier to push UTC time all the way from the db to the user javascript and operate on time then, than trying to manipulate time before sending to the user. YMMV, and it's only web dev.
This works for things that have happened. It doesn't work too well for schedules. In those cases, the timezone of the source matters, heavily.
For example, a task that occurs "daily at 15:00" does not always happen every 24 hours. When DST comes into effect, the interval shortens to 23 hours once.
Or, at "2:30 am" in the continental US can occur twice in a day, or not at all. Of course, even that isn't guaranteed, if you're in AZ.

Even "notify me in exactly 24 hours has its own complications. Leap seconds will screw up your day (as will the vague request of "exactly 24 hours").

Corner cases, the bane of simplicity everywhere.

To this day, knock on wood, I have had success at voluntarily avoiding those issues ^^ (aside from school assignements)
There are many kinds of data that get saved as time. For some, yes, it's better to add and remove the TZ at the interface. For some the TZ carries meaning by itself, and it must be stored and keep constant everywhere. For some the TZ carries meaning, but the time must be converted for display.

Know your data, and most of your problems get easy.

Yes, that's the crucial distinction -- whether the TZ, with its coarse encoding of the geography of origin, carries significance to the consumer of the data.
I agree - UTC is the way to go. The point of the example was to illustrate how simple could be detrimental to a significant portion of your userbase.

How about another example? You're building an android application. Let's pretend there's an API in the latest version of Android that reduces a dozen lines of code down to one function call - ShinyNewMethod().

You can use that ShinyNewMethod() call. It's certainly simpler.

But the vast majority of android devices in use are not running the latest OS. So the NewShinyMethod(), while being simple, will cause your app to not work for them.

Hopefully the framework designers figured a way to have this auto-reverse engineer for older devices, but that's not always a guarantee.

This results in funny things like android support library.
But no, what about UTC leap seconds? Use TAI! :)))
> I think it's more in the spirit of the article to say, forget timezones, use UTC millis everywhere. If the server doesn't speak in timezones, then you eliminated all bugs where the server mishandles timezones.

That's a flagrant example of "simple and wholly incorrect". If you don't store timezones, your future dates will eventually turn out incorrect when timezone offsets change e.g. create a meeting at 9AM local, store as UTC, country decides to not follow DST that year bam your reminder will ping an hour early or late.

Or a day off when the country decides to jump across the international date line (https://en.wikipedia.org/wiki/International_Date_Line#Samoan...).

The UTC example allows the user to translate output using localised tools.

The solution isn't incorrect, it is modular.

> The UTC example allows the user to translate output using localised tools.

So you provide an alarm clock which is works as neither a clock nor an alarm.

> The solution isn't incorrect, it is modular.

It's either not correct or not a solution, either way it's useless.

I also like how proponents of "simplicity at all cost" apparently assume/assert the composition of two systems is no more complex than either, and that there is no additional complexity to the composition layer.

most applications don't need to schedule events into the future though and its a smart strategy if all you need to worry about is the past
For very specific cases? Sure, but none of the comments talking about UTC everywhere cares to specify this rather important bit.

Unless restrictions are specified I will assume we're talking about the general case, and for the general case it's just plain wrong.

from my point of few its the other way around.

There are very few applications that need to schedule events into the future, and that is literally the only situation where you have to worry about the timezone.

Btw, keeping the timezone is insufficient as well if you're building a calendar/scheduler. If the user changes the timezone after scheduling the event... do you keep to the old one and alert him whenever, or do you adjust? There are a lot of edge cases with schedulers -- yet as i said before, most applications don't schedule into the future. They're mostly just doing things right now or within the next few minutes and keeping a log of their actions.

> from my point of few its the other way around.

> There are very few applications that need to schedule events into the future, and that is literally the only situation where you have to worry about the timezone.

My experience is the exact opposite: there are few applications which only store past dates, and in those said date is usually indicative/barely even relevant and could just as well be part of a freeform comment or removed entirely.

Indeed. During my visit to India in February this year, I didn't even realize they were at a half-hour time-zone offset, all thanks to the miracle of modern technology (and hardworking people maintaining it).
Came here to say the same thing. OP doesn't know what he's talking about.
The old yarn I'd heard for years is close to this.

Make it work. Make it work right. Make it work fast.

In that order.

Now it could be argued that "work right" can be read as "make it (work right)", or "(make it work) right", or both, but I think the point of this saying is that the "fast" part should always come later.

Yep, this is the version I heard. I think the distinction between "make it work" and "make it work right" is important, though - where "work" means "solve the problem", and "work right" means "solve the problem in a robust and reliable manner."

If your software doesn't solve the problem, it's useless, no matter how correct or fast it is. Once it solves the problem, then you can work on making it bug free and elegant. Once you're done with that, only then should you look at making it fast.

Note, of course, that "make it fast" refers to gratuitous optimisation. If it's too slow to solve the problem, then it doesn't work, and that needs to be fixed.

A similar adage states the rules of code optimisation:

1) Don't.

2) (For experts only) Do it later.

3) Profile before optimization.
4) After thinking how to get beat bang for buck when optimizing. Profilers don't always pinpoint the culprit.
I have a hard time seeing how profilers don't at least point you in the right direction. Or are we using different definitions of the word? They tell you how much time is spent in any given section of your program. If 80% of the time is spent in 20% of the code, it's usually a safe bet that's where you should start looking!
Profilers have limitations like anything else, and it's possible to be pointing the flashlight in the wrong place. I probably wouldn't include that as a list item.

By the by, is there more than one kungtotte on the Internet? It took me a minute to think why that name was so familiar, but then I remembered watching a few hundred Beaglerush videos.

There must be more than one, because I've never heard of Beaglerush.

I've used this handle for a long time though (20 years or so), so it's all over the internet.

Kk, thank you for indulging my curiosity. Beaglerush is a very humorous Aussie who is notable for a video series on the Long War mod for the game XCOM. Long War turns a moderately challenging game of thirty or forty hours into an extremely complex, impossibly difficult ordeal of at least 400 hours per game. Beagle apparently often uses the handles of his friends as character names, and one of the best/worst parts of XCOM is that it's really good at making you care about the little blobs of pixels you order into virtual mortal peril, so to me kungtotte is like, the hero of 100 missions :)

If you're into strategy games, XCOM is good, and Long War is matchless. However, Beaglerush is actually surprisingly entertaining even if you don't care for his subject; the girlfriend is still not much into the game, but after the first couple episodes she insisted on watching the other hundred-thirty-odd videos. It's probably not everyone's cuppa, but it could be a thing.

Oh, that's me then :) I didn't put two and two together because I knew him as just beagle, and it was a long time ago that I hung out with him. We played together in a gaming community known as ShackTac or ShackTactical, playing a game called Armed Assault/ArmA.

Way back when a bunch of us put our names into a custom name file for XCOM so people could make campaigns featuring ShackTac people instead of generic dudes. I completely forgot about that until you reminded me. It must be five years since I talked to him :)

Oh cool :) Yeah, I did get the impression that the name list represented more former acquaintances than current ones, and I think he got pretty burnt out on YouTube generally. He seemed like a pretty great guy, and the series was excellent, so it seems you have a small measure of reflected glory at least :)
Generally I agree, but after you've picked off a few low hanging fruit, you'll end up with the profiler pointing at the function which does most of your grunt work, at which point you'd expect that to take up most of the time.

Once you've got this pretty optimized and it's still taking up the lion's share of your execution time, you have to look elsewhere (probably changing your overall approach or applying some higher level optimisation) to improve things further.

Yes, at that point you typically get to apply far reaching architectural changes or switch out the algorithm.

Sometimes it is quicker to start with just that instead of "polishing a turd". You can get it to be shiny but still nowhere near as shiny as gold.

Hope the code is testable and reasonably easy to modify. Otherwise it's going to be a rewrite.

The profile is then useful as a benchmark on real data. If you have enough time, you can turn that into a high level performance test.

The make it work fast portion isn't controversial. Premature optimization and all that...

The "make it work" implies a level of correctness & performance that is acceptable, which is why any subsequent steps are after thoughts.

The missing part is "make it workable". By that I mean reasonably easy to modify. This may or may not involve simplicity but usually involves modularity and lack of hard impingement interdependencies - weak coupling.

If you skip that, you will relatively quickly reach the point of a full rewrite.

Sometimes I try to implement a common task in a new library, and I find that if I chain these 4 API calls and then extract some data with a regex, it will do what I want. I rarely actually implement the task this way though, because I know it's a common task, and there must be a better way. Sure enough, after some more research and reading of documentation I find one simple API call that does what I want.

I take this to the extreme, I probably wouldn't implement the complicated API / regex chain without 4+ hours of reading documentation and other research. It bothers me that much. If it seems like a simple and common task, I refuse to believe that there isn't a simple API call already to do what I want, I just have to find it. Sometimes, the simple API call really doesn't exist though, and you have to do what you can, with some comments explaining why.

I've noticed some developers will implement the 4 API chain followed by a regex as soon as they find it, and never give a second thought that there might be a simpler way.

This is good, because that process now becomes the API's problem and not yours. Their tests cover it, not yours, and it makes your work simpler.
So long as the software will only be shipped after all three stages, it's a useful metric to go by, I guess. It prevents you getting overwhelmed by the myriad amount of decision-making that writing software is. And also, so long as you accept the fact that you will need minor or major redesigns by the time you get to the "fast" version that is production ready.
as I understand it, both simplicity and correctitude are requirements for most programs, but imo the article talks about the design, the writing process, where you should focus first on writing a program that's not more complex than it needs to be, because you are likely compromising the other parts too. this doesn't mean incorrect code is acceptable.

in any case, I think it's also worth mentioning that the article is probably talking about "incorrect" as in "accidental bugs", not as in "purposely ignoring the complexity of the problem". with the idea of preventing over-engineering rather than dismissing the specifications of a valid solution

If you cannot achieve correct without simple, redefine correct.
> If you cannot achieve correct without simple, redefine correct.

More hand-waving.

I disagree. I just can't get more specific without specific cases to examine. Like others have pointed out, take the matter of timezones: if "correct" is defined as "handling timezones", you should instead store time in UTC everywhere and redefine "correct" to be "convert times to local time when displayed and back again when input", which can be accomplished with much smaller, simpler, and focused tools.
>", you should instead store time in UTC everywhere

It is not possible to store UTC unambiguously on the db server for all future local wall-clock times. (Previous comment about the erroneous assumption of "UTC everywhere" being a "simple solution".[1])

Therefore, redefining "correct" to be "store UTC everywhere" achieves the exact opposite: an incorrect and buggy program. That's because the "universal" in Universal Time Code doesn't apply to governments changing DST and Time Zone rules in the future.

Pure UTC doesn't have enough metadata to encode future unknowns. For correct handling with zero loss of data, one must store the user's intended "wall-clock" time in his local TZ in the db.

There's irreducible complexity when dealing with user-specified appointment times so an uncompromising fixation on programming a "simple" implementation with pure UTC-on-dbserver and localtime-at-only-at-browser-Javascript ... will lead to a broken calendaring program.

[1] https://news.ycombinator.com/item?id=10990240

You're right, future times are more complex and might require more attention to detail. But I think you can still achieve the requirements in a simple way, perhaps by storing it as UTC + lat/long and running a script to update future dates when someone changes their rules.
> But I think you can still achieve the requirements in a simple way, perhaps by storing it as UTC + lat/long and running a script to update future dates when someone changes their rules.

Congratulation, your emotional refusal to deal with zoned datetimes has led you to a non-standard ad-hoc reinvention of timezones, your misguided quest for simplicity and obstinate rejection of reality has thus led you to a system which is definitely more complex, probably less correct and likely less performant than if you'd just done the right thing in the first place.

>a simple way, [...] and running a script to update future dates

I've commented previously that it's not a good idea to change the rows of UTC times in the database.[1]

Designing "system correctness" to depend on on the reliability of a correctly written SQL statements completing atomic transactions for millions of rows is not a good idea. In addition to batch db updates of UTC being extremely fragile, it's also not simple.

(It's fascinating to note that the multiple programmers independently arrive at the approach to update database rows of UTC times. There's something about it that's cognitively satisfying that attracts repeated reinvention.)

[1] https://news.ycombinator.com/item?id=10991894

The event's time has changed, though. The local representation of when it will occur has not changed, but if you set a timer today and they change their timezone tomorrow, the timer will expire at the wrong time.

We should store the actual time of an event and update it when the scheduled time changes.

>, the timer will expire at the wrong time.

A countdown timer is a runtime concept.

Storing pure UTC and/or intended_localtime_plus_TZ in the database is a static concept of data-at-rest.

A timespan/timer is a different abstraction than a desired point-in-time.

Depending on the use case, the correct timer/timespan value can be derived from pure UTC (e.g. scientific celestial events) -- or -- user_specified_localtime_plus_TZ (recurring yoga class at 5:30pm every Wednesday, or take medication every morning at 7:00am).

For user calendaring and scheduling of social appointments, storing pure UTC will lead to data loss and errors. Instead of complicated mass updates of millions of db rows, it's much more straightforward to take a stored localtimeTZ, and then calculate an up-to-date UTC time at runtime, and then derive a countdown timer from that. The key insight is that the best time to use UTC is when the users need that timer at runtime -- and not when they store the row in the db.

(comment deleted)
> We should store the actual time of an event and update it when the scheduled time changes.

I would love to see some (simple) code which will send a single alert to me at 1:30 am and another at 2:30am. My client registered me as MST (-7) when I set these two alarms in Feburary.

Of particular note for corner cases: Nov 4th and Mar 10, 2019.

The "scheduled time" will change, for many locations, twice yearly.

EDIT: For added fun, instead consider the registration date as May 10th with the same timezone.

>You're right, future times are more complex and might require more attention to detail. But I think you can still achieve the requirements in a simple way, perhaps by storing it as UTC + lat/long and running a script to update future dates when someone changes their rules.

Seriously? That, in your opinion, is simpler?

>I disagree. I just can't get more specific without specific cases to examine.

You should probably mention somewhere that you're the author of the blog post under discussion. And it looks like you're going to make a reputation for yourself as the guy who argues that it's more important for software to be simple than for it to function correctly.

Good luck with that.

"redefine correct" = "find a better version of the problem". Software development starts with requirements analysis.
Some problems are just hard and complicated. You can try to make code that dissects it into lots of simpler pieces, but then they have to be put together in a complicated way. Someplace in the code there will be some point of irreducible complexity. In my experience.
In my experience you need a paradigm shift then. If your earth centric approach is creating a need for epicycles of complex code, then you need to paradigm shift to heliocentric which eliminates the epicycles and replaces them with a simple elliptical model.
I'm willing to believe that there are sometimes problems that are inherently complex, but I find that much of the time the problem is complex only because you've insisted it be complex. If you take a step back and re-examine the problem you often see that redefining the problem makes everything much more simple.
Having software people interact directly with the user/customer (internal or external) can be very useful because minor changes in requirements can make a huge difference in implementation complexity.
> But the correct assertion is there's 37 time zones (as of this writing). So, the simple solution results in a third of your potential user base having issues.

That assumes your user base is evenly distributed across time zones. The US has 6 time zones, but if you only handled 4, you'd cover 99.3% of people.

The US has more than 6. There's a big difference between Denver and Phoenix, even though they're both "Mountain Time". Grouping them together would be overly simplistic. Things like "weekdays at 10 A.M." mean different things half the year.
Noop is pretty simple...
And if the opening statement of the article is to be considered correct, noop is by design the only way to do things since its simplicity will always win against trying to create more complex code that does the task.

"The single most important quality in a piece of software is simplicity. It’s more important than doing the task you set out to achieve. It’s more important than performance. The reason is straightforward: if your solution is not simple, it will not be correct or fast."

It praises a quality that is great as an add-on, not really by itself. Pretty sure everyone prefers a complex thing that "does the task" than a simple one that doesn't.

Simple "helps". Simple never "does". I think the author's values are a bit mixed up.

He made up his mind, don't confuse him with facts.
I don't think what you're saying is in conflict with the OP:

> The complex problem comes later, and it’ll be better served by the composition of simple solutions than with the application of a complex solution.

Complicated problem domains can be made into simple ones by breaking them down into their constituent components. You can solve time zones by having 5000 Rube-Goldberg-esque lines of if/else-if statements, or you can organize the system into simple components that build on each other.

At any given component or level the problems are clear, simple, and identifiable, and the complexity arises as the components join to form abstractions upon which higher levels operate.

Or you can end up with a mishmash of components instead of statements.

Premature abstraction is a kind of premature optimization, except you're not buying performance.

Bad abstractions tend to stay in for a long time.

What matters is clear delineation between functional components and weak binding, so that internals can change, and that the interfaces are relatively minimal.

  > The reason is straightforward: if your 
  > solution is not simple, it will not be 
  > correct or fast.
A very hand-wavy statement, and not always true.
"Everything should be made as simple as possible, but not simpler"
Not convinced. Simplifying a correct implementation can be easier than correcting a simple implementation. Eventually you need to find the correct model and then everything else will follow easily, but a complex implementation that does the right thing will tell you a lot more about what the correct model is than a simple implementation that doesn't do the right thing.
This title is misleading. The post actually says that the reason "simple" comes first is because without it you can't have "correct" (nor "fast", not that that matters so much). So he's not saying simple is most _important_, just that it comes first chronologically, and has the other two as consequences.
e.g. Gall's law

> A complex system that works is invariably found to have evolved from a simple system that worked. The inverse proposition also appears to be true: A complex system designed from scratch never works and cannot be made to work. You have to start over, beginning with a working simple system.

Surprisingly to some, this is neither a law nor true.

A complex system with a good workable and testable architecture will work, starting with passing the tests down to satisfying the user...

Such systems are not designed in detail but in general, and usually start with a single, simple but powerful overarching idea, which is actually quite complex to implement, but ends up working evidently well once even halfway done.

Examples would be message passing architecture, event driven programming, time tracking, microservices, reactors, literate APIs, contract programming, Model-View-* and more... Note how half of those deal with reducing coupling by adding complexity.

(comment deleted)
That's an excellent clarification since so many people aren't getting his nuanced view.

I also disagree. I have yet in my life see any programmer crank out a simple solution on the first try for anything that isn't a trivial requirement. The way I think most of us work is to create a complex solution first and to have to refactor at least a couple of times before we get to simple and elegant. That doesn't mean the complex version didn't work.

I've made plenty of code that is bug free according to the requirements. I tend to start with tests and I'm pretty good at figuring out edge cases and other ways to break my code before I've even written it, so what I end up with is pretty robust. But the first version is rarely elegant or simple. By the time I'm done with the first version I understand the problem space so much better and might throw out 90% of my original code in the first refactor. Am I the only one doing this? Sometimes it even takes weeks or months to get to simplicity. I keep understanding the requirements better and better and noticing how I could eliminate code, often after I've noticed some code I'm still not happy with and having slept on it. Sleep does wonders for seeing how to simplify.

>This title is misleading. The post actually says that the reason "simple" comes first is because without it you can't have "correct" (nor "fast", not that that matters so much). So he's not saying simple is most _important_, just that it comes first chronologically, and has the other two as consequences.

If that's what he meant, he's flat wrong. Simplicity is neither necessary nor sufficient for correctness.

The simplest code is an empty file. And yet, we live in a world with complex software. There are tradeoffs, but this article doesn't help you make them.
> this article doesn't help you make them

Sure it does. It clearly states that sometimes new features or performance optimisations have to be sacrificed to keep the software simple.

Complexity for new features or performance optimizations is simply a trade off. Sometimes one worth making.

By adding a lot more complex code can I make this blitter 100 times faster? By using this more complex algorithm can I make a "simple" string search amazingly faster?

As for features: Word has more features than Notepad. So why doesn't everyone use notepad?

That's like saying, sometimes apples are so expensive you can't buy them. Perhaps surprising, if you haven't thought of it, but it is an obviously true statement about all tradeoffs. Sometimes you shouldn't make them. Still doesn't help you figure out when you should.
Maybe it’s obvious to you but at least in my experience lots of devs when confronted with feature request rarely consider if said feature should be implemented. Most of the time they only think how it could be implemented.
My take on this from like 10 years ago:

http://www.expatsoftware.com/articles/2007/06/getting-your-p...

I came up with Readable as the top priority, followed by Debuggable and Maintainable. I suppose one could combine that into "Simple" if one liked.

But yeah, Fast was already at the bottom of the list. Even back then.

Those three properties are very highly related.

Personally I found the biggest improvement to my own software came from maintaining the same system that I wrote for 4+ years.

If I came back to a part and didn't understand it more or less immediately, then it was time to refactor it. I wrote the code I should understand what it is doing. No excuses that someone else had written bad code.

"It was hard to write, it should be hard to understand." (I was joking, of course, when I said this.) It seems to me that that's a reasonable metric for determining if something was well written or possibly well commented. I had an opportunity to go back to some of my older code about a year ago. It was scary to me what portion of it I no longer understood. Worse yet, portions of it referred to documentation that I could no longer find. The company had migrated their documentation through several storage organization technologies and no one seemed to know where the old stuff went. Luckily I ran across a retired engineer who recalled where a lot of it had been archived.

(Unrelated) When I read the article, the first thing I thought was that all of the simple programs had already been written.

I'm getting ERR_SSL_VERSION_OR_CIPHER_MISMATCH (no content displayed) on latest Chrome / Linux when trying to access the link (which force redirects me to use https). In case it's your site, you might want to know / fix.
Thanks, Cloudflare. I noticed after posting that that blog is still running without SSL, so I did the quickest thing that would work and moved it over to Cloudflare and their Fast SSL product.

Evidently, they're happy to start auto-forwarding http traffic over to https before finishing the part where https works.

I've flipped it back off for the time being. Thanks for the heads up.

and funnily enough, writing simple code is the hardest part to learn, because you can't measure it until it falls down on you

now that I think about it, that would be a good exercise for programming students. after writing a program that does a certain task, make the student rewrite it to be simpler/easier to understand/maintain. then take a look at the best solutions in the class.

I subscribe to this philosophy, but unfortunately simplicity is surprisingly difficult for most software devs. It's because if you don't have the tool in your toolbox, you won't even know it can be simplified.

I once did a code review for a function that parsed some Linux file for Ethernet stats. It was incredibly convoluted with tons of substring finding and indexing. I told the author to simplify it and he declared he already had and it was as simple as it could get. I then showed him of the existence of regex and his mind was blown.

Exactly. And this is a major reason to always be on the lookout for new things to learn.

I had a similar experience (but earlier in his coding process) and managed to change what was about to be a months long effort of adding epicycles and epicycles to code into a one week task. I have most of my library at work and constantly speak of design and theory concepts with the younger folks. Based on more recent code it's paying off.

One of the lessons I've learned (that this article echos upon) is that you should _always_ factor the "long term cost" of adding a feature.

When I first started building TrueJob (job board software), I'd add in all these really cool features that made my app -- and at the time, they felt really useful. But over time, people weren't using them, so I built more features.

But then the old features I had built broke, so I had to fix them. And then they lagged behind the quality of other features I had written them, so I had to update them. And after doing this 5-10 times (as the software evolved and I dramatically increased the complexity of my application), these features that no one used really were painful to keep coming back to, but now enough loud users were using them that I couldn't remove them.

It made me really value the projects where we polished and did just a very few things, but did those very very well -- it lead to higher customer satisfaction, and less pain in the long run for us.

Some other formulations:

- Occam's razor https://en.wikipedia.org/wiki/Occam%27s_razor

- "Simplicity is the ultimate sophistication" (Leonardo da Vinci)

- "Less is more" (Mies Van Der Rohe)

- "Make everything as simple as possible, but not simpler" (Albert Einstein)

All four of those are great! But if they are wrong, then it is still wrong no matter how simple.

Like saying leap years occur every four years. Simple!

The author assumes that simplicity is a prerequisite for correctness which is in general true but it does not say when exactly a simple system is turned into a complex system. I would say that if the complexity of your system is still under control then it is qualified as a simple system.
I think I read the OP's assertion in relation to your counterpoint like this: when thinking through the problem, your initial iteration of work is allowed to say exactly that: a leap year occurs every 4 years. However, you should do so via a semi-stubbed function (isLeapYear) as mentioned previously. I am imagining that the specifics of when a leap year occurs are not critical to your solution, only that you know when they happen. Thus, you avoid spending too much time on a detail that's eventually important, but not critical for proving your first hypothesis. In the "correct" stage you come back and improve the isLeapYear function to return correct results.

Part of attaining wizard status is not learning how to hold more of the program in your head, but instead learning how to hold _less_. This seems like an excellent step in that direction.

I would say that the last one places correctness first.

BTW, the quote is usually attributed to Einstein, but it seems he did't say it.

Simple means that there is less to go wrong, but not necessarily correctness. A seemingly simple implementation can also hide a lot of behavioural complexity behind abstraction layers. The program might not crash, but it might not behave properly either.
Think about it in terms of each choice you make.

I have a simple solution and a complex solution. Does the simple solution meet the requirement(s) before me? If so, I prefer it. Let's move on to the next requirement and consider my options again.

The alternative might be to look at your requirements, but choose a complex solution (over a simple one) because you think it might meet other requirements, either ones that have not yet been identified or ones you think are likely to happen in the future.

Are there times that the more complex solution wins? Probably. Consider you want to write a blog. You know that you can create an HTML (text) file and slap it on a web server and your blog has started. But if you've done this before, you might also know that you can throw WordPress on your server for a little more up front pain. You know you want comments and word clouds and date/time stamps and navigation. So you choose the complex solution. (You also know that you know face potential security implications, upgrades, dealing with users causing trouble with comments, having the PHP/MySQL infrastructure/hosting requirements...) Maybe you just wanted to dump your thoughts to the internet. Maybe the text file approach was better...

It may just be another way to say "avoid gold-plating your software."

Certainly don't gold plate.

But simply meeting requirements is only doing the minimum possible. Now in a government job, that's okay.

But in a real job, if you see where the simple solution is OBVIOUSLY wrong for certain likely cases not considered in the requirement, then THE REQUIREMENTS ARE WRONG, or incomplete and this should be pointed out!

“Everything should be as simple as it can be, but not simpler” aka Occam's razor.
The OP's advice, if applied in CPU industry, would be disastrous. Modern desktop/server CPUs are incredible complex... in order to drive maximum performance. Pipelining, OOO execution, branch prediction and speculative execution: these are all features that introduce tremendous amount of architectural and design complexity. In many cases, they also harm correctness, because they can lead to functional and security bugs.

And yet, if you try to compete with Intel with a CPU missing the above optimizations, you will get absolutely creamed in the marketplace. No one, not even those touting the importance of simplicity and correctness, will buy what you're selling.

Today's free market is too complex for these overly simple rules. Choosing between simplicity, correctness and performance, is a complex tradeoff that needs to be made on a case-by-case basis. Trying to find shortcuts to avoid these analyses may feel liberating... but you're ultimately only shooting yourself in the foot.

The OPs advice would probably be dissapointing if applied to making a curry too. It's fortunate he regularly used the word software throughout the blog, really.
The attitude of the CPU industry in this regard led to some recent well-publicized, very bad, and nigh-unfixable security vulnerabilities, as you might have heard.
And yet, I don't see you or anyone else committing to buy ultra-simple non-pipelined non-OOO desktop/server CPUs.

If you insist on only hiring chauffeurs who drive at 100mph, you can hardly complain when they get into a few accidents.

The main problem is that most software leans on those misfeatures as a crutch to excuse heaping layers of abstractions. Unfortunately this problem comes from several places, so it's not as easily fixed.

That being said, consider me lined up to buy one of these CPUs.

> And yet, I don't see you or anyone else committing to buy ultra-simple non-pipelined non-OOO desktop/server CPUs.

If you can find a CPU that has the same number of non-cache[1] transistors as a Intel/AMD chip, but spends them on a larger number of simple (and preferably independent/non-hyperthreaded) cores, rather than squandering them on speculative execution and ten thousand obscure model specific registers, I would absolutely buy several of them.

1: and similar amounts of cache, of course.

Intel makes them and you can buy them today, with up to 72 Atom CPU cores, e.g. (1) https://ark.intel.com/products/95830/Intel-Xeon-Phi-Processo...

Very niche products.

For massively parallel number crunching, GPUs are much better in both performance/watt and performance/dollar. That Xeon Phi 7290 delivers up to 3.45TFlops, costs $3200, and consumes 245W. Compare with GeForce 1080Ti 10.6 TFlops, $700, same 250W.

For general purpose software they don’t work particularly well either. Most IO interfaces is serial, SATA, PCI-X, they have very few wires going to CPU. If you’re IO bound and you don’t have enough single-thread performance you’ll struggle to saturate the bandwidth, doable but very hard.

Also for general-purpose software latency matters. Namely, input to screen latency for desktops and mobiles, or request to response latency for servers. Get Windows or Android tablet with Intel Atom Z8300 (available for $80-100), and see how it performs, it has 4 very similar cores (minus AVX-512), and frequencies are very similar, too.

https://www.intel.com/content/www/us/en/processors/xeon/xeon... shows at least six volumes of datasheets, and I still haven't found a instruction set refence. I have found https://www.intel.com/content/www/us/en/processors/xeon/xeon... (helpfully labeled "Datasheet, volume 2", rather than anything related to it's contents) which describes a subset of the aformentioned ten thousand random control registers. So no, Intel does not make [simple cores], it makes heaping piles of shit complete with malware ("Intel® Management Engine") buried at D22:F0 on a interal PCI bus.

It isn't simple, it's designed to be incorrect (and even the parts that are supposed to be correct aren't), and I'm not surprised it fails on fast as well.

> I still haven't found a instruction set reference.

X86-64, SSE, AVX, AVX-512, AES-NI, etc. Their key selling point is software compatibility.

> Intel does not make [simple cores]

The cores are quite simple by today’s standards; otherwise Intel wouldn’t be able to pack 72 of them on a single chip. IME is unrelated to the cores, it’s a separate piece of silicon.

But if you don’t like the IME and don’t need backward compatibility with x86, maybe you’ll like this: https://www.qualcomm.com/products/qualcomm-centriq-2400-proc... But again, performance benefits of the architecture (48 simple cores) is questionable, GPUs are way faster for parallelizable number crunching, and you need single thread performance for almost everything else.

> X86-64, [etc]

So it has the ten thousand x86 and x64 registers in addition to the ten thousand ?PCI registers?

> The cores are quite simple by today's standards

That's my point; today's CPUs don't have "ultra-simple" as a option (at modern feature densities).

> IME is unrelated to the cores

Fair point, I probably should have added "and doesn't have technicalities like builtin malware" to my original post.

> https://www.qualcomm.com/products/qualcomm-centriq-2400-proc...

This looks interesting, although I'll need to research a bit more (and "SOC - Features - Integrated management controller" isn't encouraging). Thanks!

A counter-anecdote: The features you listed started shipping (from Intel & MIPS) in microprocessors in 1996, 22 years ago. Intel's out-of-order Pentium Pro was beaten by the in-order DEC 21164 the same year.

Also, there's the case of Intel losing to in-order ARMs in mobile. First with XScale, and later on with the in-order Atoms. (https://appleinsider.com/articles/15/01/19/how-intel-lost-th...)

Sure, specific optimizations in specific markets may not be worth the cost they incur. Or they may not be valuable enough to overcome other weaknesses in the project.

And yet, if someone tried to sell a server CPU today that was not pipelined, not OOO, and didn't have branch prediction, it would absolutely tank in the marketplace.

I never said that performance optimizations should always be implemented. Just that performance optimizations should sometimes take precedence over simplicity.

> And yet, if someone tried to sell a server CPU today that was not pipelined, not OOO, and didn't have branch prediction, it would absolutely tank in the marketplace.

You could sell it as a niche product for high security applications, since OOO execution is a nasty side-channel.

That's an interesting idea. A "so simple it can't have bugs" design would never win over the mainstream-market, but it might be able to find a niche among extremely security-conscious users. This might be a great project for the open-source community to take on.
In the free software world, this is not a huge problem. You can make new CPUs and recompile programs with far fewer effort. All that complexity really comes from monetization.
They did use OP's strategy in the CPU industry.

The first CPUs were simple as heck. They spent 20 years making them reliable(and faster without compromising simplicity, mostly just node-shrinks), and they've only really been complicating the architecture in the last 30 years.

How do you know how far to simplify if you don't pay attention to correctness? Simple has correct as a dependency.
You can apply this rule to the structure of your codebase/class: make a lot of small, focussed classes rather than one big class that does everything.

This way you can easily debug it, replace the logic of 1 class without breaking your whole application.

One of the best advices for me was: your function must do one thing only, and do it good.

I think clear is the most important. Most software projects I've worked on in the past decade have gotten a lot of assumptions wrong. The projects that were able to turn around quickly were the ones that could find busted logic and fix it.

I've been using rspec-inspired testing frameworks to help with this clarity. Whenever I implement some early business logic, I assert that the logic I wrote does what I expect in words using rspec style tests (I've been writing a lot of JS lately so I've been using Jest). The kind that read like sentences: "the tax component applies 2% tax to all purchases above $400." Even if the logic is initially incorrect, being clear in our incorrectness lets engineers and (more importantly) product people quickly identify incorrect assumptions.

The logic to apply that tax in this example may not be simple. Often times, business logic can't be simplified any further and needs to be a bit thorny. In those cases, clarity of purpose is much more beneficial than simplicity, and I've found writing rspec style tests, and forcing myself to translate what the logic is doing into words, helps immensely with clarity. It clarifies my thoughts before shipping code, and it clarifies our business assumptions as a whole when that code is running in production.

I do agree with the premise, but with at least one caveat: for some problems, there aren't any simple solutions. In that case it's better to choose the _simpler_ solution.
mmm... I disagree. The order should be Correct, fast, simple.

First of all the software needs to be correct, if the software produces incorrect results, then there is no point for it to exists. It needs to be debugged until the results are correct.

Then fast. The point of using software is helping the user to accomplish a task. The better help that the software can provide, the more useful is the software, and performance is often critical to help the user.

Then simplicity, simplicity help future developers to understand how the software work and how to properly maintain it. But if some complicated procedure is needed to produce the correct results, then complicated procedure it is. If a different algorithm needs to be used to speed the software, then the new algorithm it is. It is up to the next developer to study and understand what was done and why.

The purpose of software is the end user. The next developer sits second to the end user.

Sounds like a recipe to deliver hard-to-use software late. The easy-to-use software that was delivered on-time will eat your lunch, even with some bugs.
Making correct code simple(r) requires more time (eg money) than making correct code in the shortest time possible.

Simple, and still correct, is often the result of doing MORE work to achieve it.

This is great, but completely lost on the crowd if what Simple means isn't understood.

One of the best clarifications of what it means to be Simple, to put it out there, is [1]; but the key point: Simple != Easy.

Simple means minimal coupling, high-cohesion etc etc.

Yet IME many developers do not understand the distinction and mistakenly believe that easy is the same as simple, and are willing to couple the hell out of the world under some false notion of "simplicity"...

[1] https://www.infoq.com/presentations/Simple-Made-Easy

In a way, simplicity is the end result of reducing the complex and correct solution without affecting its correctness.

As in math, you come up with the "simple" solution of 0.5 only after you've realized that the "complex" solution is, for example, "sin(pi/4) * cos(pi/4)". There might be no other way to discover the simple solution.

That talk transformed the way I think about software development. I highly recommend watching it.
I fully buy the argument that code is a liability, while the underlying algorithm, or the overlying service is the asset. Following this line of thinking encourages the writing of simple code.

Far more of a problem than code complexity is the lack of systems thinking when applied to programs. Various factors (abstraction, delegation, nicer APIs, solid products, SaaS, "microservice" trends, package managers and bundlers) have encouraged offloading much of the computation and data flow to other products, whose strengths and liabilities become your own if you make use of them. One-liner lambdas might be simple code, but they're often coupled to a maze of other cloud services, and the complexity there is coming from the dependency substrate, whose shape can't even be expressed in an imperative or declarative notation like code.

In truth, code and libraries and services feed into systems, and those systems must be understandable if correctness and maintainability are a goal.

I can't really understand the equivocating tone a lot of folks are taking in response to this, and more importantly I can't wrap my head around how you could make such a statement in the first place: without correctness you've got nothing. Stating authoritatively that correctness comes after...anything is incomprehensible to me.

It's possible to have a correct solution that is neither simple nor fast, and it can be worth your while to speed up a correct solution while sacrificing simplicity. So there are trade-offs involved in the relationship between simplicity and speed, but correctness is not negotiable. Acknowledging that all software has bugs is not the same thing as throwing out correctness as your first and primary objective in implementing an algorithm, and accepting that your solution may only be partial or fail with certain inputs is fine if that is acceptably correct for the problem at hand, but ascertaining that still comes first. Preferring simplicity over complexity because it makes debugging, profiling, etc. easier is not a reason to insist that correctness can go out the window in service to simplicity--who cares if you've removed all the bloat from your code if it's wrong?

There's a sweet spot in the neighbourhood of well-defined problems and low complexity, that sings phrases like Before even trying to implement it I know it can be done right, or Even though it can't be done 100% right, that's just the nature of the problem domain. Give the users a good enough solution that they can understand, so they can use it in the way _they_ need.
"without correctness you've got nothing."

Sure... but define "correctness".

Suppose my manager comes to me with some incredibly complicated problem. It's going to take six months to solve properly. Suppose in the first three weeks I implement a program that is 98% correct, and let's say it can detect the other 2% and kick it out for a human to solve. But it clearly does not fully and correctly eliminate the problem as brought to me by my manager. Have I solved the problem?

The correct answer is not "no, because your solution is incorrect and there is no such thing as an 'incorrect solution' because all solutions must be correct to even be solutions; you have no professional choice but to spend the next 5 months and a week implementing the correct solution". The correct answer is "the question is underspecified". I need to go to the manager and work with them on the question of what the benefit of just deploying this is, what the benefit of doing it "correctly" according to the original specification is versus the cost, and whether or not there are any other in-between choices. The business may require the full solution, sure. On the other hand, your manager may be inclined to thank you profusely for the 98% solution in a fraction of the time because it was far more than they dreamed possible and is way more than enough to make the remaining 2% nowhere near the largest problem we have now.

"Correctness" is only fully defined in a situation where the spec is completely immutable. Specifications are almost never completely immutable. So for the most part, everyone in this conversation using the word "correct" without being very careful about what they mean are not using a well-defined word.

It's all about costs and benefits, not correctness and incorrectness. For nearly two decades, Python's sort algorithm was technically incorrect: http://envisage-project.eu/proving-android-java-and-python-s... Does this mean that any program that used Python sort was worth "nothing", because it was not correct? Obviously this is absurd (in practice at least), so correctness must be understood in terms of costs & benefits to make any sense. And such an understanding must also be grounded in an understanding of the mutability of requirements as well, to make any sense of the real world.

From this perspective, it honestly isn't even 100% clear to me what prioritizing "correctness" over everything else would even mean. That we are slaves to the first iteration of the spec that comes out, no matter what? (Obviously not, but I can't come up with anything better that it might mean.) Correctness can't be prioritized everything else because it can only be understood holistically as part of the whole process. There is no way to isolate it and hold it up as the top priority over everything else. And there is no way for the correctness of a bit of software to exceed the scope of the specification itself, almost by definition, which in the real world tends to put a pretty tight cap on how correct your software can even be in theory, honestly.

This is the most important part, and people advocating “correctness first” are missing this point.

“Simplicity first” means having a minimal skeleton code with glaring weaknesses, unimplemented features, and bugs, but having a simple design that sets you up well to absorb the inevitable shitstorm of changing priorities, pivots, revised performance constraints, feature wishlists, budget, deadlines, etc., and to manage extensibility, integration, or abstraction needs as they arrive in random, ad hoc ways.

Usually project stakeholders don’t care about absolute functional correctness, meeting performance criteria, or completeness until far far later in a project lifecycle, after those requirements have been thrashed around and whimsically changed several times.

Early on, they care about a tangible demo apparatus and solid documentation about the design and tentative plan of implementation. They want to see steady progress towards correctness & performance, but generally don’t care if intermediate work-in-progress lacks these things (often even for early releases or verson 1 of something, they’ll prioritize what bugs or missing features are OK for the sake of delivery).

In terms of interacting successfully with the business people who actually pay you and determine if your project lives on or gets scrapped, “simplicity first” is a total lifesaver, and matters far more than any of the notions of correctness discussed here.

But this is what ddellacosta is saying. I imagine his interpretation of this argument (and I agree) is that 98% correct IS prioritizing correctness. A very fast, simple solution that is 2% correct is an unacceptable balance.
"A very fast, simple solution that is 2% correct is an unacceptable balance."

That begs the question (in the original sense) of "unacceptable". If I banged out that 2% solution in an hour, and it lacked other costs that outweighed the benefits, it may still be something we ship! It is unlikely that we'd stop there, just because the numbers as you've given are unlikely to favor it because something else substantial would have to overcome the small amount of the problem we've solved, but to be firmly confident it's "unacceptable" you'd have to define "acceptable" a lot more carefully.

I understand the deep temptation to turn to discussions of the virtues of letting bugs through or something, but the costs/benefits framework completely handles that already. If you ship a buggy piece of "incorrect" shit, well, you've incurred a ton of costs with no benefits. That's wrong, by whatever standards you are measuring costs and benefits by. There isn't a "what if your 98% solution actually has a massive bug in it because you were unconcerned about 'correctness'?" argument to be made, because if it does have a massive bug, it's not a 98% solution.

>Sure... but define "correctness".

How about this, for example:

The patient lives.

A nice snarky reply.

But I would put $10 down that if I asked you to assert that all medical software in current use that has never killed a patient because of its software issues is therefore "correct", you'd walk back hard. You'd have to be crazy to assert that all such software is "correct".

Unless you are willing to make that assertion, you don't really mean that as a definition.

This is also an example of what I mean in my cousin message about the temptation to turn this into a discussion about attention-grabbing bugs. But my framework already encompasses that. Software that kills patients is software very high on the costs side. There's a complicated discussion to be had about how to exactly quantify probabilities of failure vs. cost, but you can't have that discussion if you're stuck in a "correct or not correct" mindset.

>But I would put $10 down that if I asked you to assert that all medical software in current use that has never killed a patient because of its software issues is therefore "correct", you'd walk back hard. You'd have to be crazy to assert that all such software is "correct".

I made no such assertion. That's a straw man.

But I think I can safely assume that if the patient dies as a result of the software's functioning, that software is not "correct".

You may disagree, but I think it's preferable to have a patient kept alive by an overly-complex system than killed by a simple, elegant, incorrectly functioning one.

Not to be intentionally blunt or snarky, but I think Drew DeVault's post was a bunch of rambling, hand-waving nonsense. Until today, I wouldn't have expected anyone to seriously argue that simplicity is more important than correctness. But he comes along and makes that very argument, with a self-assured, authoritative tone, but very little in the way of concrete reasoning, and to my surprise, the number of people on HN who apparently agree with him is non-zero.

I am reminded of this classic snark from The Elements of Programming Style (1974)[0]:

"Some compilers allow a check during execution that subscripts do not exceed array dimensions. This is a help … many programmers do not use such compilers because “They’re not efficient.” (Presumably this means that it is vital to get the wrong answers quickly.)" (Page 85)

[0] https://en.wikipedia.org/wiki/The_Elements_of_Programming_St...

Usually the real reason is that such checks are pointless as they do not pinpoint the bugs. They are too late. You need a real stack trace to begin debugging such issues.

Languages like Ada Spark or Rust tend to rarely use or need such runtime checks. (they are available as an option to check unsafe code)

Others like Python and Java do check and give you traces. Not for free though.

And then you probably want something more powerful, such as a virtual machine like Valgrind; full sanitization of Address Sanitizer, etc.

Yep, and that's why "worse-is-better" is better.

I've never read Gabriel's article in such a way that this "worse" means actually, seriously, worse quality. I always thought it meant "YAGNI", "KISS", and so on.

If you can't solve a problem, maybe you shouldn't try so hard? Find a better problem! I'm confident this attitude helped me a lot in the past. Speaking as a clearly-not-a-genius programmer.