There is a (large, I believe) aspect of good code that is fundamentally qualitative & almost literary. This annoys a lot of computer programmers (and academics) who are inclined to the mathematical mindset and want quantitative answers instead.
I love dostoyevsky and wodehouse, both wrote very well, but also very differently. While I don't think coding is quite that open a playing field, I have worked on good code bases that feel very different qualitatively. It often takes me a while to "get" the style of a code base, just as a new author make take a while for me to get.
I 100% agree with this. One of the best compliments I ever got (regarding programming) was from one of my principal engineers who said something along the lines of "your code reads like a story". He meant he could open a code file I had written, read from top to bottom and follow the 'narrative' in an easy way, because of how I'd ordered functions, but also how I created declarative implementations that would 'talk' to the reader.
I follow the pure functional programming paradigm which I think lends itself to this more narrative style. The functions are self contained in that their dependencies/inputs are the arguments provided or other pure functions, and the outputs are entirely in the return type.
This makes it incredibly easy to walk a reader through the complexity step-by-step (whereas other paradigms might have other complexities, like hidden state, for example). So, ironically, the most mathematically precise programming paradigm is also the best for the more narrative style (IMHO of course!)
I had a similar experience. I was a lead on a project where the client sent a functional expert to literally (at times) watch over my shoulder as I worked. He got very frustrated after a few weeks, seeing little in the way of code being laid down. He even complained to the project manager. That's because this was a complicated manufacturing system, and I was absorbing the necessary rules for it and designing it... a process that involved mostly sitting and thinking.
When I decided on the final design and basically barfed all the code out in a matter of days, I walked this guy (a non-programmer) through the code. He then wrote my manager a letter declaring it to be the "most beautiful code he had ever seen." I still have the Post-It she left in my cube telling me that.
I have little tolerance for untidy code, and also overly-clever syntax that wastes the reader's time trying to unravel it.
And now we have languages building more inconsistent and obscure syntax in as special-case options, wasting more time. Specifically I'm thinking about Swift; where, if the last parameter in a function call is a closure, it's a "trailing" closure and you can just ignore the function signature and plop the whole closure right there AFTER the closing parenthesis. Why?https://www.hackingwithswift.com/sixty/6/5/trailing-closure-...
This is just one example, and yeah... you can get used to it. But in this example, the language has undermined PARENTHESES, a notation that is almost universally understood to enclose things. When something that basic is out the window, you're dealing with language designers who lack an appreciation for human communication.
> The functions are self contained in that their dependencies/inputs are the arguments provided or other pure functions and the outputs are entirely in the return type.
Is this just a fancy way of saying static functions?
Nope, pure functions are referentially transparent. The key idea is that you can replace the function invocation with a value and it shouldn’t change the program.
A regular static function could refer to a file, a database, or it could change some global memory, etc. So, replacing the static function (that causes side-effects) with a pure value wouldn’t result in the same program.
Side-effects are usually declaratively represented by something like an IO monad. Which in reality is just a lambda with the side-effecting behaviour in the body of the lambda.
So, to make a pure IO function you don’t actually perform the IO in the function, you return a data type (the lambda) that represents the IO to perform. This maintains the purity if the function and ‘passes the buck’ to the caller. In the case of Haskell, all the way up to its Main function and into its runtime — making the language itself pure, even if the runtime isn’t.
This isn't just a Haskell thing though. I'll write code this way in C# (and have built a large pure-FP framework for C# to facilitate this approach [1]).
Here's an example of the more 'narrative style' [2] of C# using pure-FP. It reads from top-to-bottom, keeping the related functions near each other and walking the reader through the functionality. There's also a massive removal of the usual clutter you see in C#/Java programs, getting down to the essence of the logic. It won't be to everybody's taste (as it's not idiomatic at all), but it demonstrates the idea.
This style works well for regular program logic and less well for things like APIs where there's not always a narrative you can tell.
> Nope, pure functions are referentially transparent. The key idea is that you can replace the function invocation with a value and it shouldn’t change the program.
[Edit: This is wrong: And idempotent.] Generally you can expect that you can call them as many times as you like and get the exact same result. It _feels_ very safe.
> This isn't just a Haskell thing though. I'll write code this way in C# (and have built a large pure-FP framework for C# to facilitate this approach [1]).
I think that habit from Haskell is also what allowed me to pick up Rust pretty easily. You don't run afoul of the borrowchecker much if you don't expect to mutate a lot of stuff, and especially at a distance.
That's not what idempotent means. Idempotent means forall x, f(x)=f(f(x)). Most pure functions are not idempotent. Heck, f(f(x)) doesn't even type-check for most f. The typical name given to always getting the same results is just "pure". It doesn't depend on any implicit state anywhere.
There’s a difference between simplifying a concept and stating it plainly.
I use this analogy a lot. Code can be like a novel, a short story, or a poem. A short story has to get to the point pretty quickly. A poem has to be even more so, but it relies either on shared context or extensive unpacking to be understood. It’s beautiful but not functional.
And there are a bunch of us short story writers who just want to get to the fucking point with a little bit of artistic flair, surrounded by a bunch of loud novel and mystery writers arguing with the loudest poets over which is right when they are both wrong. And then there’s that asshole over there writing haikus all the fucking time and expecting the rest of us to be impressed. The poets are rightfully intimidated but nobody else wants to deal with his bullshit.
I see where you are coming from but that's unnecessarily hostile.
> There’s a difference between simplifying a concept and stating it plainly.
You are right, but they are not mutually exclusive.
The analogy you used with novel, short story, and poem/haiku also doesn't demonstrate your point: it's not like you can compress any novel into a short story, let alone a poem. If you're into games, try equating AAA-quality 3D games to novel, high-resolution 2D games to short stories, and pixel art games to haikus: it doesn't make sense and it's ridiculous.
I respect that you are passionate about the medium you choose, but what you claimed about novels and poems, as per your own words, "they are both wrong" at best. Don't generalize your personal experience to everyone else, there are kind, hardworking people out there writing novels and poems who love short stories just as much -- maybe what you need to do is to find those people instead of spewing your unwarranted anger over them.
If you think people only get upset about things for their own self interest, then I wonder what you think about social justice.
You have a Texas Sharpshooter Fallacy in your logic. A novelist is successful if they reach an audience. Once they find it, if they stick with it they will be successful. If they’re lucky then they might switch up genres without alienating their existing readers. But not everyone gets away with that.
A software developer has one audience and they don’t get to chose it. You and I write for our coworkers. If they don’t like it we have three choices. We can leave, we can change, or we can gaslight our coworkers that our code is just fine and they are the problem.
It’s the latter I’ve seen too much of, and even if you’re not a victim of it you’re allowed to be incensed for those who are. In fact you’re obligated to do so.
> If you think people only get upset about things for their own self interest, then I wonder what you think about social justice.
That's a gross mischaracterization of what I said. If anything, I'd be really concerned if you think what you're doing is akin to "social justice", and being hostile to others is justified in the name of "social justice".
> You have a Texas Sharpshooter Fallacy in your logic.
That doesn't even make any sense.
> A novelist is successful if they reach an audience. Once they find it, if they stick with it they will be successful. If they’re lucky then they might switch up genres without alienating their existing readers. But not everyone gets away with that.
Sure, there are some people who do that and not everyone "gets away with" not sticking with what made them "successful" (what is "successful" in this context anyway and how do you measure it? Wealth? Fame? Cultural impact?).
What you said isn't wrong, but there are also plenty of counter examples to what you said. So I'm not sure what you're trying to say. That looks more like a Texas Sharpshooter Fallacy.
Nobody is forcing successful novelists with an audience to continue and stick with things. Nobody is focusing unsuccessful novelists to keep going either. You make it sound like they don't have a choice so somehow everyone has to recognize and exercise "social justice" by speaking out for them.
> A software developer has one audience and they don’t get to chose it. You and I write for our coworkers. If they don’t like it we have three choices. We can leave, we can change, or we can gaslight our coworkers that our code is just fine and they are the problem.
> It’s the latter I’ve seen too much of, and even if you’re not a victim of it you’re allowed to be incensed for those who are. In fact you’re obligated to do so.
I don't disagree with any of that and I have worked with a fair share of the gastlighting kind of software engineers that you pointed out -- much too often and much too long, and they are usually very senior engineers with authority that end up literally destroying teams.
However, none of that justifies the hostility in your initial comment and that's the only point I'm trying to make. I have worked with people who are capable of writing well-structured code for others that delightful to read and maintain. If you haven't then I hope you will some day.
Anyway, at this point I'm thinking that you should just "get to the fucking point with a little bit of artistic flair", and you are probably thinking the same in reverse. Let's just leave it there.
>> You have a Texas Sharpshooter Fallacy in your logic.
> That doesn't even make any sense.
Well that explains the long response but that’s the gist right there.
Sharpshooter fires bullets at a barn and then paints a target on the spot with the most holes. That’s how creative writing usually works, unless you’re a paid columnist and even then it’s partially true.
I think you’re mistaking using swear words with hostility. Not everyone has veins popping out of their foreheads when they call a bullshit situation bullshit.
I consider code bad if it takes more then 5 seconds to read and understand the high level goal of a function.
Doesn't matter how it looks. If its not possible to understand what a function accomplishes within a reasonable amount of time (without requiring hours upon hours of development experience), it's simply bad.
There is a call-stack depth problem here that is specific to codebases though. For one familiar with the the conventions, key data abstractions (not just data model but convention of how models are structured and relate) and key code abstractions, a well formed function is easy to understand. But someone relatively new to the codebase will need to take a bunch of time switching between levels to know what can be assumed about the state or control flow of the system in the context of when that function/subroutine is running. Better codebases avoid side-effects, but even with good separation there, non-trivial changes require strong reasoning about where to make changes in the system to avoid introducing side-effects and not just passing extra state or around all over the place.
So, I'd take "good architecture" with ok and above readability, over excellent readability but "poor architecture" any day. Where architecture in this context means the broader code structure of the whole project.
But who talked about bad architecture? Good readable code doesn't rule out good architecture.
Surely some things are complicated but even then, a dev should be able to quickly see whats going on with minimal expertise in a codebase.
They're suggesting that readability and "5-second accessibility" are essentially contextual and build on a conceptual language that might be specific to a tool, team, or project.
The novel function that might take "5 seconds to read" for the 20 people contributing to a mature project with a good architecture might nonetheless take 10 minutes for a new hire to decipher because they don't know the local vocabulary (architecture, idioms) and need to trace and interpret things elsewhere in the project and its libraries.
Meanwhile, writing implementations in a way that tried to avoid a local vocabulary altogether might make naive reads easier, but less readable to experienced team member because they're not as concise as they could be.
Your general advice to "make things easily readable" is good advice, but like with writing compelling prose or making accessible graphic design, you need to consider your audience and your choices might look different than the ones somebody else might make.
Go is a board game with simple rules but combinatorial consequences to those rules. Minutes to learn and a lifetime to master.
This is where “simple recursive data structures” can be simple to read but difficult to track and comprehend. An architecture that descends through distinct layers at least has landmarks that the recursive one does not have. If you are not at the root or the leaf you don’t really know where you are, lost in the middle.
The broader problem is "cognitive load to understand the code I'm looking at." There are a variety of factors that lead enabling that. This is a very limited example.
function isReadyToDoThing(Foo foo) {
return foo.ready
}
function processStuff((Foo foo) {
if isReadyToDoThing(foo) {
res = workflowA(foo)
res2 = workflowB(foo)
return res && res2
}
}
This might be dumb, if isReadyToDoThing is trivial, and it could be easily inlined. Or alternatively it could be a good way to self-document, or annotate a preferred approach (imagine several similar named methods). Regardless if you don't know the code, you'll want to go look at the method, especially if it is in a different file.
If `isReadyToDoThing` is only used in one place, I'd argue that it's better to inline it with an appropriately-named variable so I don't have to "go to definition" to understand what it's doing.
I think people get too caught up in "small functions" and lose the readability of code locality.
the purpose of the function should be clear from its name. if its too complex to convey this information it should have a docstring that clearly explains what it does. it's not rocket science
In the general case I fully agree. It is ideal that the name of anything clearly indicates whatever is important about that thing.
But who is the viewer of that name?
How much context can they be assumed to have? The name of the class? The name of of the module? The nature and terminology of the business that the function serves? The nature and terminology of the related subsystems, infrastructure and libraries?
There is a context dependent local optimum for how to name something. There are conflicts of interest and trade-offs have to made.
> How much context can they be assumed to have? The name of the class? The name of of the module? The nature and terminology of the business that the function serves? The nature and terminology of the related subsystems, infrastructure and libraries?
Yeah, not disagreeing with what you write but parent is talking about different type of complexity which your description/approach doesn't magically fix, I'd call it spread of complexity
Sounds like you are content to limit yourself to problems that do not contain more irreducible complexity or require more developer context than what fits within five seconds of comprehension.
That's a good rule for straightforward CRUD apps and single-purpose backend systems, but as a universal declaration, "it is simply bad" is an ex cathedra metaphysical claim from someone who has mistaken their home village for the entirety of the universe.
If you find compact language above your level of proficiency confusing, you can literally ask an LLM to explain it for you to trade efficiency for accessibility.
You sound unhappy and seem to be lashing out, and your username can only be read as an allusion to a mentally ill would-be assassin. Given those, you can maybe begin to understand why your opinions are not credible as a contribution even in relation to other anonymous people.
A small part of your comment is salvageable, though:
> Understanding what something is for is not understanding how it accomplishes it
I can think of at least one area I know something about where the 5 second rule fails - sometimes when working on a shader and optimizations for it, it takes more than 5 seconds for the person who wrote the code to describe what it's for at a high level.
If even the person who wrote the code can't meet that arbitrary constraint, other people looking at the code for the first time have no chance.
> I consider code bad if it takes more then 5 seconds to read and understand the high level goal of a function.
That's something that's possible only for fairly trivial logic, though. Real code needs to be built on an internal "language" reflecting its invariants and data model and that's not something you can see with a microscope.
IMHO obsessive attention to microscope qualities (endless style nitpicking in code review, demands to run clang-format or whatever the tool du jour is on all submissions, style guides that limit function length, etc...) hurts and doesn't help. Good code, as the grandparent points out, is a heuristic quality and not one well-defined by rules like yours.
I agree with this up to a point - having consistent code style with some sort of formatter (gofmt, black, clang-format) goes a long way to reducing complexity of understanding because it unifies visual style.
I suggest that a codebase should read like a newspaper. While there is room for op-eds in the paper, it's not all op-eds, everything else should read as a single voice.
My experience is that projects which value code formatters (and similar rulemaking) tend strongly not to have room for "op-eds", FWIW. And conversely the code bases I've seen with the best/cleanest/most-clearly-expressive code tend strongly to be the ones with fewer rules.
I think the one exception there is in some open source contexts (Linux is the archetype) which receive a fire hose of submissions of questionable quality and maintainership. There, you can use adherence to arbitrary rules as a proxy measurement[1] for attention and effort on the part of the submitter. And that I have no problem with.
But the actual value of code formatters is IMHO extremely low in practice, and the cost isn't "high", but is non-trivial.
> I meant the goal of your function needs to be grasped within a reasonable amount of time. This works for every codebase.
It really doesn't though. Here's a function of mine. It's maybe 40 lines of logic, so medium-scale. It's part of an intrusive red/black tree implementation for Zephyr. I'm fairly proud of how it turned out, and think that this code is awfully readable given its constraints.
No human being is going to understand fix_extra_red() without having already read and understood the rest of the file, and coming to it with an understanding of the underlying algorithm. Certainly I can't. I can't even get started on maintaining this code that I originally wrote within a five minute time frame, it's an hour at least every time, just to remind myself how it works:
Now maybe this is "bad code", and "good code" could exist for this problem that still meets your requirements. But... if so that's an awfully celestial definition if it's so hard to find.
This is exactly the kind of example I have in my head for code that constitutes a high level of information density. Adding abstraction and 'literate' constructs to try and make things readable is ultimately deferring the fact that understanding the code here is fundamentally about understanding a specific implementation of an algorithm, and to understand _that_ ultimately needs the reader to build their own clear mental model of both the algorithm itself and how it's been translated into a specific form.
Maybe it's a defeatist attitude, but I feel like sometimes the problem is the problem, and pushing abstractions only works to defer the requirements to understand it. Sometimes you can defer it enough to do useful work, other times you just need to understand the thing.
> Maybe it's a defeatist attitude, but I feel like sometimes the problem is the problem, and pushing abstractions only works to defer the requirements to understand it
That's also my impression and experience.
And sometimes there is no problem at all, but abstractions are still pushed too far and then a problem arises in the form of non-essential complexity.
Code that looks like it has a bug in it but doesn’t will draw the eye over, and over, and over again when fishing for how regressions or bugs got into the code. This is the real cost of code smells. At some point it’s cheaper for me to clean up your mess than to keep walking past it every day. But I’m going to hate you a little bit every time I do.
Consider reading kernel or driver code. These areas have a huge amount of prerequisite knowledge that - I argue - makes it OK to violate the “understand at a glance” rule of thumb.
for the codebase i work on, i made a rule that "functions do what the name says and nothing else". this way if the function does too much, hopefully you feel dumb typing it and realize you should break it up.
I find an odd overlap between people who get incredulous about function decomposition and who think cracking open a thesaurus as an architectural exercise is stupid.
I have no idea what that’s about, but I think it has something to do with “white-knuckling”.
People name things and then miss boundary conditions that matter and would have been implied by finding a more accurate synonym. And also supplementary features that the better name suggests.
I have definitely been guilty of naming functions foo_thenBarSometimes. I wince whenever I write them, but I've never really regretted seeing them later on, even after years. So sometimes it really is a perfectly good name. Sometimes there are two related functions that are often called together and don't have a succinct label for the combined operation.
It likely has some higher-level meaning other than just do foo, bar, qoo.
For example if you are calling functions "openDishwasher", "loadDishwasher", "closeDishwasher", "startDishwasher", your function should be called "washDishes". Not always that straightforward, but I believe in 95% it's not difficult to put a name on that. For the rest 5% you need to get creative, or maybe you realize that you didn't group the function calls well enough to have an atomic meaning.
Yeah, I agree in spirit but I think the answer is more ”it depends” than something where you should feel bad or something if you deviate from it. If washDishes also sends a bunch of metrics/diagnostics or updates a database of your favorite dish washing programs somewhere inside it, that’s probably fine. Otherwise you push the path of least resistance to just be vague instead, then you get a codebase full of functions with names like handle or process.
Some function can be named generally but the name is specific when concatenated with the module name and/or the package name. `load` is generic, but `app.config.load` is more descriptive.
Does this apply to all domains and all 'kinds' of code?
I feel like there's a fundamental difference in the information density between code that, for example, defines some kind of data structure (introducing a new 'shape' of data into an application) versus code that implements a known algorithm that might appear short in line length but carries a lot of information and therefore complexity.
If the algorithm is well known, it's all good as long as the function name for it is somewhat understandable.
I have to work with 200 line functions at work and it's a complete, excuse the language, shitshow.
> as long as the function name for it is somewhat understandable
But does using a function, essentially a box with known inputs and outputs, constitute actually understanding the function? What happens if you need to debug or understand the implementation of it? Now the original name has gone and you're looking at a larger number of differently-named things that hopefully communicate their intent well. But if you need to understand _those_, and so on...
This is what xmldoc/jsdoc/etc are for. If it's not 100% obvious from the name, put a summary of the function's assumptions, side effects, output, and possibly an example in the comment-doc. If you do this right, the next programmer will never have to read your source at all (or even navigate to your file! They'll hover over a method call or find it in the dot-autocomplete and see a little tootip with this documentation in it, and know all they need to know). It's an incredible thing when it works. It's a little bit more effort but I don't accept the FUD around "comments become out of date immediately because the code will change" etc. - that should be part of code review.
Always, always check in about whether it would be simpler to fix the function than to write an extended apology for it working the way it does.
While Five Whys works very well for disaster prevention, I find 3 often suffice for fixing rather than explaining an architectural wart. Often we used to need this to work this way because something else had to work a particular way, but as the product grew that is no longer true, or desirable. So you might be able to fix it or put a fix on the backlog.
>This annoys a lot of computer programmers (and academics) who are inclined to the mathematical mindset and want quantitative answers instead.
I find many syntactical patterns that are considered elegant to be the opposite, and not as clear as mathematics, actually. For example, the the ternary operator mentioned in the article `return n % 2 === 0 ?'Even' : 'Odd;` feels very backwards to my human brain. It's better suited for the compiler to process the syntax tree rather than a human. A human mathematician would do something like this:
Well of course if you have the freedom to write a mathematical expression you're going to be able to present it in a way that is clearer than if you have to type monospace characters into a text editor.
I'm not sure it's realistic to expect to be able to type a mathematical expression using ascii more clearly than you can write it by hand (or implement using special unicode characters).
Quite some years back I worked with JetBrains MPS which used a "projectional editor" instead of a text editor. It was pretty neat to be able to enter "code" as mathematical expressions, or even state machine tables or flow diagrams with actual nodes instead of a text representation.
Sadly not much has happened in that space since then, but it was cool to think about what our tools of the future might look like. (of course ignoring all the practical reasons why we're probably still using regular text files in 100 years)
I understand that you might find the mathematical notation clearer but I think it's presumptuous of you to speak on behalf of all humans, or even all human mathematicians. I'm a mathematics graduate and I find the conditional operator more readable in a program because it corresponds to what the program actually does (it checks the condition first); but I also recognize that the two notations have exactly the same information content and only differ superficially in syntax, making it entirely a matter of familiarity.
This is why code reviews are so critical: They help keep a consistent style while onboarding new team members, and they help a team keep its style (reasonably) consistent.
I love dostoyevsky and wodehouse, both wrote very well, but also very differently. While I don't think coding is quite that open a playing field, I have worked on good code bases that feel very different qualitatively. It often takes me a while to "get" the style of a code base, just as a new author make take a while for me to get.