6 ms·
I think most programmers agree that simpler solutions (generally matching "lower cognitive load") are preferred, but the disagreements start about which ones ar
by defanor 1y ago
I think most programmers agree that simpler solutions (generally matching "lower cognitive load") are preferred, but the disagreements start about which ones are simpler: often a lower cognitive load comes with approaches one is more used to, or familiar with; when the mental models one has match those in the code.
For instance, the article itself suggests to use early/premature returns, while they are sometimes compared to "goto", making the control flow less obvious/predictable (as paxcoder mentioned here). Intermediate variables, just as small functions, can easily complicate reading of the code (in the example from the article, one would have to look up what "isSecure" means, while "(condition4 && !condition5)" would have shown it at once, and an "is secure" comment could be used to assist skimming). As for HTTP codes, those are standardized and not dependent on the content, unlike custom JSON codes: most developers working with HTTP would recognize those without additional documentation. And it goes on and on: people view different things as good practices and being simpler, depending (at least in part) on their backgrounds. If one considers simplicity, perhaps it is best to also consider it as subjective, taking into account to whom it is supposed to look simple. I think sometimes we try to view "simple" as something more objective than "easy", but unless it is actually measured with something like Kolmogorov complexity, the objectivity does not seem to be there.
- brucehoult 1y ago> For instance, the article itself suggests to use early/premature returns I like premature returns and think they reduce complexity, but as exclipy writes (I think quoting Ousterhout) 'complexity is defined as "how difficult is it to make changes to it"'. If premature returns are the only premature exit your language has then they add complexity in that you can't then add code (in just one place) that is always executed before returning. A good language will also have "break" from any block of code, such that the break can also carry a return value, AND the break can be from any number of nested blocks, which would generally mean that blocks can be labelled / named. And also of course that any block can have a return value. So you don't actually need a distinguished "return" but only a "break" that can be from the main block of the function. A nice way to do this is the "exit function", especially if the exit function is a first class value and can also exit from called functions. (of course they need to be in a nested scope or have the exit function passed to them somehow). It is also nice to allow each block to have a "cleanup" section, so that adding an action to happen on every exit doesn't require wrapping the block in another block, but this is just a convenience, not a necessity. Note that this is quite different to exception handling try / catch / finally (Java terms) though it can be used to implement exception handling.
- derf_ 1y ago> A good language will also have "break" from any block of code, such that the break can also carry a return value, AND the break can be from any number of nested blocks, which would generally mean that blocks can be labelled / named. And also of course that any block can have a return value. Even in a language that is not "good" by your definition... you have basically just described a function. A wrapper function around a sub-function that has early returns does everything you want. I use this pattern in C all of the time.
- brucehoult 1y agoIt is more inconvenient to make a wrapper function for a function than to make a wrapper block for a block, especially in C where you can't lexically nest functions (not counting GNU extensions). Naturally all programming languages are equivalent, but some are more convenient than others. See the title of this post "Cognitive load is what matters".
- neutronicus 1y agoAn old C/C++ argument lol. The C people want their exit blocks, the C++ people want to write destructors. As a C++ guy I'm on the early-return side of things, because it communicates quickly which fallible operations (don't) have fallbacks. You see "return" you know there is no "else". Also, as a code-formatting bonus, you can chain a bunch of fallible operations without indenting the code halfway across your monitor.
- mattmanser 1y agoPersonally I wouldn't agree with this. I've adopted a pattern where I try to only ever return the success value at the end of a function. Early returns of success value don't feel clear to me and make the code hard to read. I think that sort of code should only be used if you need high performance. But for clarity, it hurts. Instead I think you should generally only use early returns for errors or a null result, then they're fine. Ditto if you're doing a result pattern, and return a result object, as long as the early return is not the success result (return error or validation errors or whatever with the result object). So I feel code like this is confusing: function CalculateStep(value) { if(!value) return //fine ///a bunch of code //this early return is bad if(value < 200) { //a bunch more code return [ step1 ] } ///a bunch more code return [ ..steps ] } The early return is easy to miss when scanning the code. This is much less confusing: function CalculateStep(value) { if(!value) return //fine ///a bunch of code let stepsResult : Step[] if(value < 200) { //a bunch more code stepsResult = [ step1 ] } else { //a bunch more code stepsResult = [ ..steps ] } //In statically typed languages the compiler will spot this and it's an unnecessary check if(!stepsResult) throw error //cleanup code here return stepsResult } It makes the control flow much more obvious to me. Also, in the 2nd pattern, you can always have your cleanup code after the control block.
- whilenot-dev 1y ago> one would have to look up what "isSecure" means, while "(condition4 && !condition5)" would have shown it at once You would feel the need to look up a variable called isSecure, but would not need to look up condition4 or condition5? I think the point TFA was making is that one could read isSecure and assume what kind of implementation to expect, whereas with condition4 I wouldn't even know what to look for, or I'd even struggle to hold any assumption. /* this one needs to make sense in the end */ isSecure = user.role == 'admin' /* these two do not */ condition4 = user.id <= 4 condition5 = session.licenseId == 5 > and an "is secure" comment could be used to assist skimming Those are exactly the kind of comments I'd rather see written out as intermediate variables. Such comments are not explaining to you any of the Why?s anyway, and I also tend to trust the executing code much more than any non-type-related annotating code, as comments are rarely helpful and sometimes even describe wishful thinking by straight-up lying. Intermediate variables assist in skimming too.
- defanor 1y ago> You would feel the need to look up a variable called isSecure, but would not need to look up condition4 or condition5? I assume that those "conditions" are placeholders, not to be read literally in the example (since the example is not about poorly named variables, but about complex conditions), so I did not mean them literally, either. Supposedly those would be more informative names, such as "channel_encrypted", "checksum_verified". > [...] describe wishful thinking by straight-up lying This was what I had in mind upon seeing that "isSecure" bit, too: could easily be a lie (or understood differently by different people). But taking a little more effort to check then, and/or having to remember what those variables actually mean. It is a commonly debatable topic though, where the good balance is, similarly to splitting code into small functions: people tend to balance between spaghetti code and extreme splitting. My point though is not to argue with those particular points here, but that we have no such practices/rules universally considered simple and formally stated/verifiable.
- prerok 1y agoWell, from my experience, as well as from tools figuring out complexity of functions (so, seems to be accepted and not my personal preference), nested ifs add to cognitive load. So, we know that early returns are easier to understand. In a lot of code reviews, I am in debates how to name things. I know the juniors are cross with me, as if it's bike shedding, but it's important for clarity when reading/debugging. --- Disregard: I also have to disagree with you on HTTP error codes. Those are well documented and cannot be counted as obscure and unnecessary knowledge that not everybody needs to understand. They have to. It's their freaking job. If they don't, they should not write nor review any HTTP related code.-- EDIT: Above paragraph: It seems I misread you comment, sorry.
- Cthulhu_ 1y agoLikewise, some people prefer ternary statements for short checks; I want to agree because ternaries are one of the first things you learn after if/else/while/for, but at the same time... they're a shorthand, and shorthand is short but not necessarily more readable. For one-off things like value = condition ? a : b I don't mind much, but I will make an issue as soon as it spans more than one line or if it's nested.
- mabster 1y agoI particularly don't like ternaries with side-effects or control flow. In particular with control flow I prefer it always tabbed in otherwise sometimes I miss it -- if statements are much better for this.
- davemp 1y agoI prefer it as long as there’s no side effects. You get tighter semantics which I think helps readability (and I trust compilers to be able to handle it optimally). I find the following format to be very nice: value = (condition) ? foo : bar;
- epolanski 1y agoIndeed, familiarity is 95% the reason why one would find a solution more, or less simple.
- zakirullin 1y agoCognitive load depends on the mental models one has, yes. > The problem is that familiarity is not the same as simplicity. They feel the same — that same ease of moving through a space without much mental effort — but for very different reasons. Every “clever” (read: “self-indulgent”) and non-idiomatic trick you use incurs a learning penalty for everyone else. Once they have done that learning, then they will find working with the code less difficult. So it is hard to recognise how to simplify code that you are already familiar with. This is why I try to get “the new kid” to critique the code before they get too institutionalised! This was explored in the latter part of the article.
- dsego 1y agoSometimes an established pattern is easier to understand than the improved version. In that case convention is better, for example comparing http codes directly instead of giving them names, since those are easy to read for anyone who's ever done web dev.
- mikepurvis 1y agoA lack of nuance about this kind of thing is part of what enrages me when ChatGPT tries to tell be a planned change or design is going to be “elegant” or “simple”. It’s like… maybe yes, maybe no, but those are not binary terms and throwing them around like that makes it sound like an enthusiastic intern sucking up to his sensei rather than a digital brain whose thoughts are formed by having ingested billions of lines of real life code.
- lazylester 1y agoyour rage suggests that you think ChatGPT is making value judgements about elegance or simplicity. You surely know where it picked up those concepts don't you?
- mikepurvis 1y agoErm yes, I’m being a bit tongue in cheek if that’s not obvious.
- nurettin 1y agoUnfortunately there is no one definition of simple, because developers call simple "whatever they are used to" rather than an objective measure such as "the least branching" or "the fewer lines of code". The best I can come up with is "code that causes the least amount of scrolling" which is hard to measure and may be mitigated with IDE help.
- PaulStatezny 1y ago> programmers agree that simpler solutions...are preferred, but the disagreements start about which ones are simpler Low ego wins. 1. Given: The quality of a codebase as a whole is greatly affected by its level of consistency + cohesiveness 2. Therefore: The best codebases are created by groups that either (1) internally have similar taste or (2) are comprised of low ego people willing to bend their will to the established conventions of the codebase. Obviously, this comes with caveats. (Objectively bad patterns do exist.) But in general: Low-ego → Following existing conventions → They become familiar → They seem simpler
- Sleaker 1y agoI don't think this necessarily is accurate I've come into a lot of projects that no one understands well, but everyone continues to follow the same bad conventions that already exist which just adds to the problems. Ex: deep nesting, no early exit, deep object inheritance.. this happens because a lot of developers don't want to rock the boat, AND because they don't have the skills to unwind the complexity in a manageable amount of time without also causing serious problems.