
Programming Patterns I Like - fagnerbrack
https://www.johnstewart.dev/five-programming-patterns-i-like
======
vast
Nested 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.

~~~
ravenstine
Ironically, it could have been written far more concisely and clearly by using
IIFE, if-then statements and reordering the logic:

    
    
      const result = (() => {
        if (!conditionA) return 'Not A';
        if (conditionB) return 'A & B';
        return 'A';
      })();
    

Yes, I know, I "need" to use curlies. But there's really no reason why someone
shouldn't be able to quickly make sense of this. The ternary, on the other
hand, is unnecessarily cryptic.

~~~
correct_horse
Can't help but call out Rust here for it's (imo excellent) syntax.

if condition {do_x()} else {do_y()}

Basically there's no need for line breaks between if/else, but also they
decided not to add a ternary conditional operator.

~~~
ravenstine

      if (condition) { do_x() } else { do_y() }
    

That's the JavaScript version of what you wrote, and it's nearly identical.
JavaScript doesn't require line breaks.

~~~
kbp
The significant difference is that in Rust, like in Lisp, `if` is an
expression, so if-else essentially is the ternary operator.

    
    
        const x = cond1 ? a 
                : cond2 ? b
                : c
    

becomes in Rust

    
    
        let x = if cond1 { a }
                else if cond2 { b }
                else { c }

------
eugf_
I 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.

~~~
ced
When 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
This 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 :-)

~~~
AstralStorm
And when conditions are not direct don't forget to add Lisp level of
parentheses. I bet one time you will forget and everything will explode.

------
seanwilson
> 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
The lodash one might be inspired by the Python itertools "partition" recipe,
[https://docs.python.org/3/library/itertools.html#itertools-r...](https://docs.python.org/3/library/itertools.html#itertools-
recipes)

TFA’s inlined reduce version is much less readable than calling a function
which wraps this behavior up.

~~~
seanwilson
It's common in functional programming libraries too, usually with the same
name. See e.g. OCaml, Haskell and Standard ML.

------
quadcore
I'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._

~~~
amelius
But break/continue statements are also like goto, and to be consistent they
would need to be eliminated as well.

~~~
agumonkey
The more I read about for loops, non local exits.. the less I want to go back
to imperative programming.

------
oaiey
This 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.

~~~
novaleaf
Re 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
The 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.

------
deathanatos
> _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#Anothe...](https://en.wikipedia.org/wiki/Data-
driven_programming#Another_interpretation) and the referenced
[https://homepage.cs.uri.edu/~thenry/resources/unix_art/ch09s...](https://homepage.cs.uri.edu/~thenry/resources/unix_art/ch09s01.html)

> _the important part is moving program logic away from hardwired control
> structures and into data_

~~~
zwkrt
Of 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?

------
tylerhou
You 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);
        }

~~~
paulddraper

        const 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;
        }, [[], []]);

------
DivisionSol
1\. 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
I 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.

~~~
setr
If 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...

~~~
jacoblambda
Yep I'm dumb. That's what I get for writing that snippet last night with a few
drinks in me.

As for comparison against switches, I find that switches are very limited in a
lot of languages (looking at you C/C++). The problem with the if else chain as
well is that most code formatters seem to blow it up into the fully expanded
form which kills readability.

------
janpot
I 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
iirc php is the only (popular) language with messed up ternary-if
associativity, it works as you'd expect pretty much everywhere else

~~~
stevenhilder
I 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)

------
jorangreef

      contentTypes[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
I 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)

------
mgkimsal
"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"?

------
darkkindness
As 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
The 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).

------
amelius
Note: this is mostly about style at the syntax level, not about design
patterns at the architectural level.

------
baolongtrann
I 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

~~~
masswerk
Regarding nested ternaries, while not recommended, short circuit evaluations
may be even more readable:

return ( (cA && cB && "A & B") || (cA && "A") || (cB && "B") || "neither" );

~~~
foota
Quick, it's another case of Javsacript Stockholm Syndrome!

~~~
masswerk
:-)

However, this would also work in Perl, etc, (anything featuring short-circuit
evaluation and type coercion for evaluating conditions.)

------
Nition
Re 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.

------
SeriousM
I 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
It’s also dangerous in JavaScript where properties of the Object prototype can
mistakenly be dereferenced instead of the default/fallthrough case.

------
ulucs
I 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
Proxy 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.

------
MarkMc
The '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));

------
needle0
When 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?

~~~
thristian
There's a very old programming style rule about only ever having one entry and
one exit to a function. This rule dates back to the days of assembly
programming, where any 'function' could just jump directly into the middle of
some other function, or jump out before the end. This made understanding and
maintaining code super-difficult, and this rule made a lot of sense.

Eventually, "structured programming" languages like C and Pascal were
invented, where you couldn't just leap from one arbitrary part of the program
to another. In these languages, the compiler enforced that each function had a
single entry point, solving part of the problem. However, for C in particular
the rule was still kind of useful - all the resources allocated during the
function needed to be cleaned up by the end, so it was still good practice to
have a single exit point that always did whatever cleanup was required.

These days, most languages have a garbage collector that will automatically
clean up for you. Languages that don't have a garbage collector, like C++ and
Rust, generate all the deallocation logic at compile time (the RAII idiom).

If you're using a language designed after the early 1980s, the "single entry
and single exit point" rule is either wholly irrelevant, or does more harm
than good.

------
nec4b
Replacing 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.

------
laythea
Early returns are good because they also save a lot of indentation.

------
m0ther
re: 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 :\

------
pteredactyl
Thanks for sharing this. I didn’t know about ‘reduce()’. Only thing I’d add is
to use TypeScript.

------
Volt
Should have applied item 4 to item 3. I don't really know what's going on
there.

------
enriquto
I 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
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.

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
> 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
Ah, 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

