Hacker News new | ask | show | jobs
by dimaor 1060 days ago
One thing I have learnt very well from rewriting a legacy PHP code written by amateur teams is: never ever nest ternary operations.

maybe it's only me, but it's really hard to reason about this.

7 comments

I see this opinion voiced all the time, and I can never understand how people can struggle with something like nested ternaries. Surely a straightforward use (i.e. not a weird edge case) of very basic syntax shared by most widely used programming languages shouldn't cause much of an issue?

It should also be easier to reason about nested ternaries than an equivalent set of nested if-elses, because at least with ternaries you know that every branch is an expression resulting in some value (and statically typed languages ensure that these values have the correct type), whereas if-else blocks can contain anything, are likely (and in most languages (which lack if-else expressions), forced) to mutate things, and have no guarantee of producing the result that you were expecting, unlike ternaries (especially in statically typed languages).

Should ternaries be left or right associative?
Right associative! It's just one of the many rites of passage for people working with PHP to get bitten by its left associative ternaries. Naked nested ternaries are deprecated now, but maybe one day, PHP can have right associative ternaries.
Except when it's not, like in perl.
Not sure what you mean, but ternaries are right-associative in Perl just like most other languages. PHP is the odd one out.
Except when they're not.
Unfortunately there is no if-expression in JS so sometimes it's awkward _not_ to use ternaries in multi-line statements - for instance, when writing in an expression only context like a string interpolation or JSX. It's also just annoying to not be able to assign conditionally without using it, instead of a more clear and readable if/else. It's one of the more annoying nits of the Algol legacy.

Oh, but fully agreed, nested ternaries is right out. In fact, usually in those other cases too, it's just annoying that there's not a good alternative.

If you want the equivalent of if-expression in JS, I think it is much better to make a separate function with a number of returns. In this case the function is already there:

  function deriv(exp, variable) {
    if (is_number(exp)) {
      return 0;
    }
    if (is_variable(exp)) {
      if (is_same_variable(exp, variable)) {
        return 1;
      } else {
        return 0;
      }
    }
    if (is_sum(exp)) {
      return make_sum(deriv(addend(exp), variable),
          deriv(augend(exp), variable));
    }
    if (is_product(exp)) {
      return make_product(deriv(multiplier(exp),
              variable),
          multiplicand(exp)))
    }
    return error(exp, "unknown expression type -- deriv");
  }
It's subjective, but seconded (switch statements are also great for this because they make fall-through logic more obvious).

I'll add a couple of things onto this: early returns are very helpful for me in avoiding nesting if statements (although that's less applicable in this specific example).

  function op(cond) {
    if (cond) {
      //do something
    }
  }

  function op (cond) {
    if (!cond) { return; }
    //do something
  }
And it's good to remember that you can basically stick functions anywhere including inline, so it's not necessarily a requirement to take a function like this and move it to a top level as a private function. If you're only using it in one place you can just define it and call it anonymously.

And don't be afraid to still use ternary operators non-nested. There's a sibling comment complaining about the nested if statement. If that really bothers you, you can still do:

   if (is_variable(exp)) {
      return is_same_variable(exp, variable) ? 1 : 0;
   }
Refreshing to see some love for early return. Often people like to say they are an anti pattern, but then you have to maintain each layer of if nesting in your head (as they are sometimes off screen) when reasoning about code in the middle instead of handling edge cases first and leaving the rest of the method for the core/common case.

  if (is_same_variable(exp, variable)) {
    return 1;
  } else {
    return 0;
  }
should be

   return +is_same_variable(exp, variable)
Yeah, though might want to use Number(is_same_variable(..)) for additional clarity.
Get rid of the else in the is_variable(exp) block and this is a lot more readable than the nested ternary version and seems just as easy to transcode from Scheme. Darn.
Which is one of the reasons why it's difficult to interpret javascript as suitably functional to apply SICP to it.

"Languages do not differ in what they make possible, but in what they make easy."

Or use a switch statement?
There's something satisfying about converting if/else tables to switch statements. I find them so much more readable.
a function _if(cond, a, b) would probably have been better.
Beat me to it :)

It's not the biggest thing in the world, and I don't want to distract from the rest of the book, but this is a situation where writing a one or two line helper function:

  const _ = (cond, a, b) => cond ? a : b;
would have made the code much more readable without much downside that I can see -- at least to my subjective opinion. Maybe I'm missing something.

Edit: comment below correctly points out that if it's important for you to avoid immediate evaluation, you'll need to wrap your conditionals in functions.

JS is not lazily evaluated so that means `a` and `b` would both be evaluated regardless of the result of the cond expression. To make a proper version you have to complicate things by calling it like this:

  _(cond, () => a, () => b)
And _ becomes:

  const _ = (cond, a, b) => cond? a(): b();
And it does matter in this case when looking at the last condition which signals an error (does not return an error value if I understand it correctly). In which case your _ would raise an error even when not appropriate.
That is an excellent point, thanks for pointing that out.

I'm not sure it matters here, the error you're pointing out looks to be getting returned (unless I'm misunderstanding what the book intends the `error` function to do), and creating an Error in Javascript is fine, it doesn't break your program until it's actually thrown.

Edit: just looked at your comment again, and you're saying it does actually throw the error rather than returning it :) So double-corrected on my part :)

But your point stands regardless. There will be scenarios where what you're talking about matters -- JSX also follows this pattern of immediate evaluation and yeah, I see errors from that plenty of times. So it's good to mention.

There is an example in the book showing why this would not work, in short it is because js is not lazy
Haven't had the pleasure of dealing with this, but I'm told PHP has a uniquely broken idea of ternary: http://phpsadness.com/sad/30
If you use a good indentation is not bad... more concise and clear than if/then/else
One thing I have learned from reading $any_lang code written by professional teams is: use nested ternary operations where it makes sense to do so, to tersely express a series of if/else conditions that simply return a value for each condition.
There is strict reasoning why ternary expressions are, in fact, logically better than a typical if statement.

The reasoning is because ternary operations eliminate programming singularities.

Example:

   var x;
   if (someExpression) {
       x = True;
   }
   //var x is undefined
Example 2:

   var x;
   x = someExpression ? true : false;
   // use of ternary expression prevents var x from ever being undefined.
The ternary expression forces you to handle the alternative case while the if expression can leave a singularity. A hole where x remains undefined.

Some people say ternary expressions are less readable. But Readability depends on your "opinion." There are no hard logical facts about this. So it's a weak-ish argument.

It is actual fact (ie not an opinion) that ternary expressions are categorically more Safe then if-statements. Therefore, they are factually better in terms of hard logical metrics.

This is the reasoning behind nested ternary statements. It's also one of the strange cases in programming where superficial qualitative attributes of "readability" trumps hard and logical benefits. The overwhelming majority of the population will in fact find many nested ternary operations highly unreadable and this "opinion" overrides the logical benefit.

Usually though, to maintain safety and readability I just alias boolean statements with variable names. The complexity of a conditional expression can be reduced by modularizing parts of it under a symbolic name, you don't necessarily have to reduce complexity by forcing the conditional into the more readable bracketed spatial structures favored by if-statements.

Example:

   isXgreaterThanY = x > y;
   isWlessThan2 = w < 2;
   isSubExpressionTrue = isXgreaterThanY & isWlessThan2 ? False : True;
   isFactNotTrue = isSubExpressionTrue ? False : True;
Yes technically the ternary expressions are still nested in a way here, but when reading this code you don't have to dive in deep past the symbolic name.

Imagine if you have a boolean condition that relies on multitudes of these sub expressions. By defining the final statement as a composition of Named ternary expressions you eliminate the possibility of a hole;... a singularity where one alternative wasn't considered. In my opinion this is the best way to define your logic.

If-statements, imo, should be reserved for functions that are impure (void return type).. code that mutates things and touches IO which is something you as a programmer should keep as minimal and segregated away from the rest of your pure logic as possible.

Example:

     if true:
        sendDataToIO(data)

     //the alternative of not sending data to IO is not a singularity. It is also required... a ternary expression makes less sense here. 

Last but not least: The ternary expression is also a bit weak because it only deals with two possible branches: True/False. The safest and most readable primitive to deal with multiple branches of logic is exhaustive pattern matching. Both Rust and Haskell have a form of this. In rust, it is the match keyword.
I implemented the Monkey programming language as described in the book Writing An Interpreter In Go. Over time I extended it to support different things, and one of the additions I made was to add support for the ternary operator.

In my implementation nested ternary operators were a parse error.

I've been thinking about diving into that book next. Would you recommend? My alternative is Crafting Interpreters by Robert Nystrom.
I would recommend, the writing is clear and concise, and there's a lot of coverage and emphasis on testing.

That said Nystrom's book has adorable drawings, and feels more "passionate".

I've read both, but I only followed the go-book because I was learning Golang at the time. No regrets.