11 ms·
Programming Patterns I Like
- seanwilson 7y ago> This pattern is nothing really special and I should have realized it sooner but I found myself filtering a collection of items to get all items that matched a certain condition, then doing that again for a different condition. That meant looping over an array twice but I could have just done it once. Write a generic function for this instead? Lodash has one like this called "partition" for example. I try and avoid explicit for loops as much as possible as they tend to make code harder to follow and state manipulation is a big source of bugs. Most operations on collections can be decomposed into some easy to follow combination of map, reduce and filter instead (this is a filter variation). Nice article!
- jacobolus 7y agoThe lodash one might be inspired by the Python itertools "partition" recipe, https://docs.python.org/3/library/itertools.html#itertools-recipes https://docs.python.org/3/library/itertools.html#itertools-r... TFA’s inlined reduce version is much less readable than calling a function which wraps this behavior up.
- seanwilson 7y agoIt's common in functional programming libraries too, usually with the same name. See e.g. OCaml, Haskell and Standard ML.
- np_tedious 7y agoThe "call it builder instead of reduce" comment here was interesting. My knee jerk response was to disagree, being that reduce is well known to mean fold / accumulate. However I've seen a lot of js usages where it does things like modify structures in the containing scope and isn't an FPish fold at all, so a name like that could probably make a lot of sense. Nonetheless for this particular usage, I'd agree that a more specific partition function like lodash's would be an improvement.
- deleted 7y ago[deleted]
- amelius 7y agoNote: this is mostly about style at the syntax level, not about design patterns at the architectural level.
- vast 7y agoNested ternaries are horrible. They add an additional burden to think about operator precedence, which is actually a hard thing especially if you switch between languages regularly. They are harder to extend for those reasons and often it is just stupid to handle condition prerequisites outside of the nesting of an if statement. Conditional logic is the third hardest thing just after variable naming and cache invalidation. There is no excuse to make it harder to understand.
- dukoid 7y agoWell in the example in the article they are more chained than "really" nested... So I think with adequate formatting, they can reasonably replace switch/case or if/elseif expressions (in contrast to statements) for languages that don't have them...
- vitus 7y agoI always thought one of the main arguments for the guard pattern ("early exits" in the article) was _specifically_ to reduce nesting, so I find it interesting that both guard patterns and nested ternaries were mentioned. For complicated nesting that's nontrivial to simplify in this fashion, you should then think about extracting logic into helper functions. (The example is also a bit misleading, since the logic flow between the nested ifs and the chained ternaries are subtly different.) In the provided example, const result = !conditionA ? "Not A" : conditionB ? "A & B" : "A"; is really equivalent to: const result = null; if (!conditionA) { result = "Not A"; } else if (conditionB) { result = "A & B"; } else { result = "A"; } which I don't think is that bad (look ma! no nesting!). You could also use a function, especially if the logic is too complex to flatten reasonably: function checkConditions() { if (!conditionA) { return "Not A"; } // A is true from here onwards. if (!conditionB) { return "A"; } // B is also true from here onwards. return "A & B"; } const result = checkConditions();
- y4mi 7y agoYou're code example is technically nesting btw. That language doesn't have an "elseif", so you're actually writing the shorthand for if(!a) { ... } else { if(!b){ ... } else { ... } } Your point stands nonetheless. I just had to smile seeing that example
- ulucs 7y agoI feel Proxies could be a cleaner way to handle default cases in using object literals to replace switches: const cases = new Proxy({...cases}, { get: (obj, val) => obj[val] || () => {...default case} }); It does not add any lines to the definition, and saves you from writing `|| cases[default]` all the time and bugs from forgetting to write it. Additionally, you can also use 'default' as a case.
- TheAceOfHearts 7y agoProxy is slow and unnecessary. Passing around a wrapped object that behaves in this way would be exceptionally surprising. Just move this behavior into a function if you find yourself having to repeat something many times.
- williamdclt 7y agoor write a `get` function, surely that'd be cleaner and more explicit
- janpot 7y agoI used to write php and JavaScript at the same time. Turns out nested ternaries get evaluated differently in both languages, leading to some subtle bugs sometimes. I will never write those without parentheses again.
- uryga 7y agoiirc php is the only (popular) language with messed up ternary-if associativity, it works as you'd expect pretty much everywhere else
- stevenhilder 7y agoI believe you're correct; I'm not aware of any other language with a left-associative ternary. This will be deprecated as of PHP 7.4[0], and will cause a compile error in PHP 8.0. However, instead of switching straight to right-associativity, PHP will require explicit parentheses to disambiguate nested ternaries. [0] https://wiki.php.net/rfc/ternary_associativity https://wiki.php.net/rfc/ternary_associativity
- Nition 7y agoRe the switch example, another possible solution is the command pattern, where you encapsulate the action in an object. e.g. (in C# because my JavaScript isn't good): interface IContentType { void CreateType(); } class ContentPost : IContentType { public void CreateType() { return console.log("creating a post..."); } } class ContentVideo : IContentType { public void CreateType() { return console.log("creating a video..."); } } void CreateContentType(IContentType contentType) { // Instead of a switch, you can now do: contentType.CreateType(); } Mainly useful if you want to do more than one thing depending on content type, of course. It's overkill if you'd only need a single switch anyway.
- laythea 7y agoEarly returns are good because they also save a lot of indentation.
- enriquto 7y agoI disagree strongly with the "no foo variables" rule. If you put meaning into a variable name that you use twice, then you are repeating yourself, which is very bad. Thus, variable names should NEVER be meaningful (except maybe when they are used only once). It is better to name your variable "x" with a clear comment of its meaning upon its declaration. Moreover, mixed-case symbols suck big time.
- MobileVet 7y agoAre you familiar with the concept of ‘coding your documentation?’ Code often changes without the comments being updated so making the code as informative as possible has lasting benefits. It is much easier to use descriptive variables than keep the comments up to date. Additionally, descriptive variables allow anyone to read and more quickly understand the code.
- enriquto 7y ago> Are you familiar with the concept of ‘coding your documentation?’ Code often changes without the comments being updated so making the code as informative as possible has lasting benefits Yes, and I agree 100% with it. It is much better to write self-documenting code than comments. I just contend that descriptive variable names are not the way to self-documenting code. Of course, I'm talking about local variables whose scope should never go beyond a single screen of code. Globally accessible identifiers (e.g. functions) should of course have very long and descriptive names. > Additionally, descriptive variables allow anyone to read and more quickly understand the code. This does not ring true to me, but maybe it depends a lot on the context. At least in numeric mathematics, you always want to name your numbers, vectors and matrices using single letters (as they appear in the corresponding paper). Using "cute" descriptive names hinders a lot the readability of formulas.
- MobileVet 7y agoAh, I think the context of your programming and mine are quite different. I don’t do any mathematical formulations beyond basic manipulations. I can see where someone writing complex mathematical formulas in C would definitely benefit from single character variable names
- baolongtrann 7y agoI agree with the 1st pattern. The 2nd one, there's a caveat about space/GC here. The 3rd one depends a lot on the situation. 4th one is too obvious for it to be called a pattern, but I agree with it. 5th one is, meh, not really easy to read. I prefer his "bad" example. It could be less verbose, e.g, if (!conditionA) result = "Not A" else if (conditionB) result = "A & B" else result = A
- foota 7y agoPersonally, I'd prefer to go with early return, I think. Depending on what the conditions mean semantically. It would be: if (conditionA && conditionB) { return "A & B" } if (conditionA) { return "A" } else { return "Not A" } return "neither A nor B" I think this comes from a general dislike of more than one else on an if and a dislike of nesting.
- scns 7y agoSame concerns about #2 here, prettier code but worse performance, Form over Function.
- masswerk 7y agoRegarding nested ternaries, while not recommended, short circuit evaluations may be even more readable: return ( (cA && cB && "A & B") || (cA && "A") || (cB && "B") || "neither" );
- eugf_ 7y agoI know that is a matter of taste, but nested ternaries are not straightforward to understand as the author try to convey. The example shows two nested levels, and I need to simulate the program flow for a while in my head to really understand it. Definitely not desirable for my everyday programming sessions. However, simple and short ternaries are welcomed.
- jacobolus 7y ago> need to simulate the program flow for a while in my head to really understand it. This is a matter of practice. It doesn’t take that many times thinking about it to understand how this works in general (it’s always the same).
- epse 7y agoI agree completely, the provided example is very confusing
- tobr 7y agoThat’s true of anything you’re not used to. A traditional for loop is very confusing the first dozen times. Simple chained ternaries like in the example really aren’t hard to reason about. They are a sequence of conditions, each followed by a `?`. The first condition that evaluates to true will return the value after the `?`. If none evaluate to true, the value after the final colon is returned. You can chain as many ternaries as you want and follow the same simple rules to understand what they return. EDIT: The example in the article is rather unfortunate though, as the if statement is nested in a different way than the ternary, for unclear reasons.
- ced 7y agoWhen formatted like this: result = (cond_A ? 1 : cond_B ? 2 : cond_C ? 3 : 5) they look quite a bit nicer than the if/else equivalent IMO.
- mikekchar 7y agoThis is actually nice because in some languages a case statement is a statement and not an expression -- so you can't assign the value easily. +1, would use this in production code :-)
- deathanatos 7y ago> 2. Switch to object literal I've generally called a much more general form of this "data driven programming". (The data here being the mapping.) See: https://en.wikipedia.org/wiki/Data-driven_programming#Another_interpretation https://en.wikipedia.org/wiki/Data-driven_programming#Anothe... and the referenced https://homepage.cs.uri.edu/~thenry/resources/unix_art/ch09s01.html https://homepage.cs.uri.edu/~thenry/resources/unix_art/ch09s... > the important part is moving program logic away from hardwired control structures and into data
- zwkrt 7y agoOf course the logical conclusion of any data driven programming technique is to build a DSL and a parser. I always think that if my solution is starting to look like a compiler I’m doing something right since I’m modeling the domain and allowing end users to solve their own problems. After all, what is a compiler/interpreter of a Turing complete language if not (provably) the most flexible data-driven program?
- darkkindness 7y agoAs a future educator interested in how people think about code, I like these submissions! Whenever things like functional style or language syntax pop up on HN, they tend to create a big stink in comment threads, as people defend their (obviously different) tastes, perhaps by providing contrived code examples, or focusing on why Alice's code is "unreadable" to Bob, rather than understanding "that's just what works for Alice." Honest, unprovoked opinions like in this blog post (like "Hey, this works for me!") are a breath of fresh air.
- nosianu 7y agoThe brain rewires - literally. So whatever you are used to - you are used to it because your brain wired itself to recognize those patterns. So obviously, when you look at something you are not used to, you are not wired for, it takes a lot longer to process because a lot more of the brain needs to get involved. However, with some time the brain rewires and what was alien now seems completely natural. Of course, this is within larger limits, you cannot adapt to everything and not to everything at the same speed. Anyway, my point is, for any such comment people make they should make this test: Do I feel how I feel because of the above, or do I have actual hard knowledge that there is a deeper effect? Meaning, is my comment simply a report of my current wiring state (so, mostly useless to others), or do I have to share actual insights? Then there's what I wrote in another comment here, about disagreements on waaayyyy to general topics, where people might not actually disagree nearly as much if there was a specific context, but since the topic is so broad everybody comes with their own imagined scenario, depending on their own past experiences, which may very well be best served with very different approaches, but since the discussion remains abstract everybody keeps defending their opinion. Which may actually all be right - for the concrete scenarios they all have in their heads. For example, there are several commenters here arguing against nested ternaries. I did the same for the longest time. But TypeScript's conditional types require - and officially advertise - exactly this pattern. The way they write it it looks perfectly clear, and I found myself agreeing with there actual concrete code (i.e. types; I posted the link in another comment here).
- DivisionSol 7y ago1. Yes, love avoiding nested if-statements. 2. Yes, similar to my love my avoiding nesting, I too like avoiding indentation. Switch -> Case -> Statement+Break vs Object -> Key/Value. 3. I think this example dips a bit into the arcane. It's not that I can't read it, but you have a reduce starting with two arrays and some conditional logic. I don't often run into it, but I might try to think of where this can apply further. 4. Yes. Even though I love short concise code, I want the variables and function names to be clear to their meaning. When I read acceptable abstract code I expect the pieces to read like a set of instructions. Ideally the nicely named functions do what they say they do, and no more. 5. Hard pass. That ternary looks like a blob of characters and I have to think very hard about the logic going on. Not a... if b then a & b else a... I don't like picking on specific examples inside these kinds of posts, but on first pass: if(!conditionA) return "Not A"; if(!conditionB) return "A"; return "A & B"; Probably give it a nice snappy function name so there isn't a bunch of conditional logic in the middle of a function, when I just want to know the state of two booleans, (with a caveat).
- jacoblambda 7y agoI think the problem with #5 is the formatting and use case. Ternaries are really useful for certain things and can make code much easier to read but they are also super easy to misuse. The big thing I find is that ternaries allow for greater code density and better alignment of similar elements. The style I find works best is as follows. Note that alignment of the ? and : characters (and depending on the code style, the parentheses) is vital for the legibility of this style. x = (condA) ? (trueA) : (falseA) (condB) ? (trueB) : (falseB) (condC) ? (trueC) : (falseC) (condD) ? (trueD) : (falseD) (condE) ? (trueE) : (falseE) : (final case); Mind you this has it's uses mostly in low level stuff but I find it a hell of a lot easier to read than a giant if else chain.
- julienreszka 7y agoyes
- setr 7y agoIf I'm reading it right, those false cases shouldn't exist; it should be: x = (condA) ? (trueA) : (condB) ? (trueB) : (condC) ? (trueC) : (condD) ? (trueD) : (condE) ? (trueE) : (final case); and the equivalent if/else chain should be: if (condA) {x = trueA } elif (condB) {x = trueB } elif (condC) {x = trueC } elif (condD) {x = trueD } elif (condE) {x = trueE } else {x = (final case) } In which case it doesn't really get you much readability-wise, since the formatting consistency appears in either case; the main benefit really is that ternary is an expression rather than a statement It’s definitely superior, since it’s still less noise all around, but almost worthlessly so. And then the same pattern gets covered by switches too...
- tylerhou 7y agoYou really should just use a for loop over reduce. It’s much easier to understand, shorter, and is more performant: const truthy = [], falsy = []; for (const i of values) { if (i) truthy.push(i); else falsy.push(i); }
- ulucs 7y agoA better option (for me, who likes fp) would be writing a group_by reducer once and apply it to any situation at hand group_by = f => (acc, item) => ({...acc, [f(item)]: [...(acc[f(item)] || []), item]}); const {true: truthy, false: falsy} = values.reduce(group_by(Boolean), {}); Though as long as you don't need to keep writing for loops for each grouping you do, group_by using reduce or for loops doesn't really matter.
- paulddraper 7y agoconst truthy = [], falsy = []; for (const i of values) { (i ? truthy : falsy).push(i); } If for some reason you did want to use reduce (and I see no reason to), it would be const [truthy, falsey] = exampleValues.reduce((arrays, i) => { arrays[+!i].push(i); return arrays; }, [[], []]);
- deleted 7y ago[deleted]
- ElFitz 7y agoThat really is more a matter of taste than facts
- needle0 7y agoWhen I found out about the 1st pattern I liked it, but at another workplace I was advised to avoid it since it creates multiple exit points for the function, leading to confusion. I'm not quite convinced of that and tend to agree with the author of this article, but are there any other arguments against it?
- Khoth 7y agoI think having multiple exit points is good when the early exits are all at the beginning for trivial parameter validation, but likely to be bad if they're buried in the middle of the function. In either case, there's the disadvantage that if you want to do printf debugging and make the function print "function foo returned 'false'" when it returns, early return makes it a pain. (Generally not so much a problem for real cleanup work, since your early returns are likely before the function's actually done any work that needs cleanup)
- frogcoder 7y agoAlthough I've been avoiding multiple exists, I don't think the the patten is that bad, just more of personal taste. However, I think it's import to let the readers of the function know what the function mainly do first, insead of the edge cases. In functional thinking, it's actually very helpful to think of the edge cases and get them out of the way first. When put in coding, I like to think it's more helpful to let the readers know the purpose of the function first, they could dig deeper later if they choose to.
- Gibbon1 7y agoBack in the mid 80's my more computer sciency coworkers gravely warned about 'bad things' that would happen if you had multiple returns. I found multiple returns were useful places to stick break points[1]. Like when you're trying to track down a rare error condition that happens every so often. Like once a week. [1] I use a debugger cause I'm a small brained primate.
- sombremesa 7y agoIt's hard to argue against one-liner early exits at the very beginning of the function.
- quadcore 7y agoI'm very surprised that early returns are still commonly used and advocate in 2019. In my opinion, they are obviously terrible. I'll try to convince you. First the practical, in my experience, a big function with many early returns has a bug or has a convoluted logic, 100% of the time. I've literally made a living and a career by untangling big functions with early returns. I've really saved many situations and code just by doing that. The reason this happens is that early returns are gotos and gotos are harder to follow. Also, you can't move easily (think copy/paste) code than has early returns, which means that the code is defacto harder to refactor. So in practice, code with early returns stays in place and grows. Early returns are artefacts of the assembly jump instruction. They shouldn't even be in a high-level programming language because it makes no sense to have jumps in such a language. They make the programmer lives in the assembly language world (succession of instructions that update a state) instead of the wonderful world of high-level programming (expressions). If you interested by what I'm saying, you should try removing all returns, continues and breaks from your code, you'll see what wonderful things happens. It's literally magic. Things becomes so much more expressive without them. Which is the point, high level programming is about gaining more expressiveness over assembly code. Early returns is one of the big pillars of bad inexpressive entangled convoluted programming. And what is people reason for using early returns? It makes less indentations... You may wonder why early returns doesn't sound that much like a big deal after all. And you are right, there is no big deal. No big deal in terms of what the market reward. Now if you care about the art of programming, this is a huge deal imo. edit: thank you for the fair discussion. I would like to insist that obviously, if your code is more expressive, you will take better decisions and at the end, what I'm saying is that you will end up with a totally different program. Early return or not is not about cosmetic. It's about writing better program.
- ubercow13 7y agoHow would you rewrite the first example?
- quadcore 7y agoreturn rawData && rawData.length > 1 ? rawData.map((item) => item) : [] In many other cases, I would use a result variable. It's more expressive and more importantly you can move that code (the result variable may become just another variable in a bigger function). (note that in many programming language like lisp or haskell you wouldn't need the result variable because everything is an expression) Granted, it looks like no big deal in many many spots. Until it is, particularly at scale when you can't refactor anything because everything is full of early returns.
- SeriousM 7y agoI disagree with the "object literal" because it hides the traces when which is taken. Sometimes it's better to write more self explaining code instead saving a vew lines.
- ryanpetrich 7y agoIt’s also dangerous in JavaScript where properties of the Object prototype can mistakenly be dereferenced instead of the default/fallthrough case.
- jorangreef 7y agocontentTypes[contentType] || contentTypes['default']; Don't do this. Untrusted user input could be anything, such as "hasOwnProperty" or "toString", for example: contentTypes['hasOwnProperty'] // [Function: hasOwnProperty] contentTypes['toString'] // [Function: toString]
- your-nanny 7y agoI always thought the urge to replace switches with objects was fashionably bad advice. I get that the object literal approach feels more functional, and that forgotten break statements will cause havoc tho. Seems like a safeswitch construct is in order, that does not allow dropthrough. something like: safeswitch(s) { case 'alpha': calc(alpha(x), 2); case 'beta': calc(beta(x), beta(x*x)); } else { // default case here calc(x); } alternatively, switch could be modified so that passthrough is not the default, and so, instead of a break statement one would use a passthrough statement to enable that behavior (opt in instead of opt out)
- steve_adams_86 7y agoWhat are the implications of this? What could go wrong outside of runtime errors? When I've used this pattern in the past I've used a reusable 'getter' object which implements `hasOwnProperty` on itself for the sake of being sure I'm getting only the stuff I want. But it's not quite as concise as what's shown in the example, apart from getting the default internally rather than externally using ||. I didn't do this for security though, I did it for sanity - I don't want to get false positives when accessing, and the prototype has a ton of properties.
- mgkimsal 7y ago"I started deferring to ternaries and nested ternaries and I found I was able to quickly understand at a glance what was happening." That sort of reads like "I was able to read my own code". That's a good start, but can other people also "quickly understand at a glance"?
- Volt 7y agoShould have applied item 4 to item 3. I don't really know what's going on there.
- nec4b 7y agoReplacing switch, which is compile time construct (converts to an if-else statement or a lookup table or a binary search) with an object which is a run time construct (hash table) is pretty inefficient at least in compiled languages.
- pteredactyl 7y agoThanks for sharing this. I didn’t know about ‘reduce()’. Only thing I’d add is to use TypeScript.
- oaiey 7y agoThis object / function switching is horrible. Obfuscates code reading a lot. Why lambdas and not named functions. Same for nested ternaries. This is the kind of code which no one will like afterwards to maintain. Early exits are fine when they are used for invalid input/use cases. But not for regular algorithmic outputs. This would hide cyclomatic complexity. Unfortunately, not understood by the author once you understand the reduce example. I am not experienced enough with reduce to make a statement about the array pattern... But it looks good especially when the language has array deconstruction.
- AstralStorm 7y agoEarly exits should be used carefully - security critical code should not rely on a single check due to TOCTOU errors. Essentially every check hit after the primary - and there should be such cold redundant checks - is a critical error - not necessarily in "System Halted" way but high severity. These may light up on normal use in concurrent cases too if there are bugs. Even hardware bugs. Guard macro/template/local assertion is almost always better, the key is to not produce too much distracting syntactic noise or work against language features. Redundant rare checks are eaten by branch prediction. (Needs care too, call when Spectre is fixed.)
- novaleaf 7y agoRe Object/functions, I think his example is a bit flawed too, but not for the reason you give. A lambda vs named function doesn't really change his pattern at all. The big problem for me is that it's offloading the logic for picking to the caller, so that if they forget to default null to ```contentType["default"]``` everywhere there will be a bug. putting the choice+ default fallback at the callee side reduces this boilerplate.
- oaiey 7y agoThe reason I complain about the lambdas is the lack of readability what the function does (the name), the lack of a speaking stack trace and finally in most languages this pattern also allocates memory.
- MarkMc 7y agoThe 'One loop two arrays' example looks like premature optimisation to me. Going through the list twice is much easier to understand: let exampleValues = [2, 15, 8, 23, 1, 32]; let truthFn = (x) => x > 10; let truthyValues = exampleValues.filter(truthFn); let falsyValues = exampleValues.filter((x) => !truthFn(x));
- m0ther 7y agore: switch to object literal. Run a perf test comparing execution speed on the two strategies. I've had to defend switch like three times this year; there's a reason it exists, it's super easy for compilers to optimize :\