A Compelling Case for the Comma Operator
Basti Ortiz

Basti Ortiz @somedood

About: Web developer. Open-source contributor. Learner. Writer. Mentor. Leader.

Location:
Philippines
Joined:
Oct 19, 2018

A Compelling Case for the Comma Operator

Publish Date: Sep 6 '24
83 24

The comma operator is one of the lesser-known operators in C-like languages such as JavaScript and C++. Essentially, it delimits a sequence of expressions and only returns the result of the final one.

const a = 1;
const b = 2;
const c = 3;
const result = (a, b, c, 4, 5, 6, true);
console.log(result); // true
Enter fullscreen mode Exit fullscreen mode
if (false, true) console.log('hello'); // hello
Enter fullscreen mode Exit fullscreen mode

It's natural to ask then: when would it ever be useful to cram multiple expressions in a single line? Furthermore, even if it were useful, why would a comma-separated sequence of expressions (in a single line) be more readable and maintainable than a semicolon-separated sequence of statements (across several lines)? When should we prefer one over the other?

These are questions that I have struggled to answer over the years, but now I think I finally have an answer. In this article, I present a compelling case—perhaps the only one frankly speaking—for the comma operator.

A Motivating Example

Let's first talk about the conditional ternary operator. As seen below, if the condition is truthy, it evaluates value. Otherwise, it evaluates another. There is emphasis in the key word "evaluation" here because the branches only execute when their condition is met.

const result = condition ? value : another;
Enter fullscreen mode Exit fullscreen mode

For most cases, it's neat and pretty. Where it falls apart, however, is when we need to do more complex logic in between the branches before returning the conditional value. At this point, we resort to this unfortunate perversion:

let result; // Uninitialized! Yikes!
if (condition) {
    // Do some complex stuff in between...
    doSomething();
    // ...
    result = value; // Actual Assignment
} else {
    // Do other complex stuff in between...
    doAnotherThing();
    // ...
    result = another; // Actual Assignment
}
// Hopefully we didn't forget to initialize `result`!
Enter fullscreen mode Exit fullscreen mode

Now there are many issues with this formulation.

  1. The result is uninitialized at first. This is not inherently evil, but an easy tried-and-tested way to avoid bugs due to undefined is to just always initialize variables.
  2. The initialization of result is literally at the bottom of the branch—far detached from its declaration.
  3. By the end of the conditional, we better hope that result is surely initialized. If not us, we better hope that our teammates equally enforce that. If not now, we better hope that future developers uphold that, too!

There is a way around this limitation if we insist on using conditional ternary expressions. We just have to refactor the code into functions. That's definitely easier said than done. This gimmick gets old real quick!

function computeWrappedValue() {
    // ...
    return value;
}

function computeWrappedAnother() {
    // ...
    return another;
}

// How cumbersome!
const result = condition ? computeWrappedValue() : computeWrappedAnother();
Enter fullscreen mode Exit fullscreen mode

Expression-based programming languages (such as Rust) have a more elegant solution. By reclassifying the if statement as an if expression, each branch can be evaluated and thus return values that can later be stored in a variable.

// A conditional ternary operator thus looks like this. Each branch
// returns a value, which is captured by the `result` variable.
// We thus ensure that `result` is always initialized by construction.
let result = if condition { value } else { another };
Enter fullscreen mode Exit fullscreen mode
// If we wanted to do something more complex, we use the same syntax.
let result = if condition {
    do_something();
    // In Rust, the last expression without a semicolon is the value
    // that will be "returned" by the overall `if` expression.
    result
} else {
    do_another_thing();
    another
};
Enter fullscreen mode Exit fullscreen mode

Can we emulate this in C-like languages? You've likely long foreseen where I'm headed with this, but yes!

A Compelling Case

What we want is a way to arbitrarily execute statements before returning a value within the ternary branches. Well, lucky for us, this is exactly what the comma operator is for.

// Parenthesized for clarity.
const result = condition
    ? (doSomething(), value)       // evaluates to `value`
    : (doAnotherThing(), another); // evaluates to `another`
Enter fullscreen mode Exit fullscreen mode

The neat thing about this formulation is the fact that the branch expressions are only evaluated when necessary. We effectively emulate the behavior of expression-based programming languages. Gone are the days of ad hoc wrapper functions!

But alas, we can only go so far with this technique. You can imagine that for some sufficiently large n, cramming n statements into a single line already begs to be refactored into its own function. Personally, I would already reconsider by the time n > 3. Anything higher than that is dubious construction in terms of readability.

// Maybe we should reconsider here?
const result = condition
    ? (x++, thing = hello(), doSomething(), value)
    : (++y, thing = world(), doAnotherThing(), another);
Enter fullscreen mode Exit fullscreen mode
// Okay, stop. Definitely turn back now!
const result = condition
    ? (
        x++,
        thing = hello(),
        doSomething(),
        doMore(y),
        doEvenMore(thing),
        value,
    ) : (
        ++y,
        thing = world(),
        doAnotherThing(),
        doMore(y),
        doEvenMore(thing),
        another,
    );
// Unless, of course, you're fine with this. It kinda does
// look like a Rust `if` expression if you squint hard enough.
Enter fullscreen mode Exit fullscreen mode

Conclusion

Wrapping up, we have seen a compelling case for the comma operator: complex conditional ternary operations. The comma operator shines when the branches are short and sweet, but falls out of fashion real quick after three inlined statements. At that point, one is likely better off refactoring the code.

So should you use comma operators? Honestly... yeah! Readable code is mindful of the next reader, so as long as the comma chains are never egregiously long, I would accept—and even encourage—this coding style. If we consider the alternatives (i.e., uninitialized variables and refactored micro-functions), the comma operator is not so bad after all.

In practice, I already sprinkle my own codebases with these funny-looking comma operators. Though in fairness, I rarely have a need for multi-statement ternary conditionals anyway. But when I do, I have a cool tool in my belt that concisely expresses my intent.

To that end, I rest my compelling case for the comma operator.

Comments 24 total

  • Paul J. Lucas
    Paul J. LucasSep 7, 2024

    If you do something like:

    int n = (f(), 42);
    
    Enter fullscreen mode Exit fullscreen mode

    in either C or C++, many compilers will warn you that the value of the left-hand operand of , is discarded. To silence the warning, you need to cast it to (void).

    That aside, cramming stuff into an expression and using , just makes for unclear code. Just use an if-else instead.

    BTW, you missed the obvious primary use-case for , (at least in C or C++) which is inside a for:

    for ( int i = 0, j = 0; i < m && j < n; ++i, ++j ) {
        // ...
    }
    
    Enter fullscreen mode Exit fullscreen mode

    where the second , is the comma operator and separates the ++i and ++j.

    • Basti Ortiz
      Basti OrtizSep 7, 2024

      Ah yes, that's true. I did omit that use case because I found it to be less compelling than the emulation of expression-based conditionals as in other programming languages. Though I will not deny that your example is pretty neat and useful, especially for zip-like operations.

      That aside, cramming stuff into an expression and using , just makes for unclear code. Just use an if-else instead.

      Yes, I agree, but I have raised my arguments against this: the primary one being uninitialized variables whose initialization is far detached from the declaration. If the only safe1 viable alternative for this is the comma operator (as shown in the article), then I'm all for it!

      That's why for less than three inlined statements, I find the comma operator quite useful and readable for conditional ternaries. Anything more than that, though, is dubious construction that is worth refactoring into dedicated functions or otherwise. The if-else construction would honestly be my absolutely last resort.


      1. By "safe", I mean code that is less likely for us now, us in the future, other developers now, and other developers in the future to forget to initialize the variable in all branches. 

  • Steve Schafer
    Steve SchaferSep 8, 2024

    I would say that, rather than a compelling case for the comma operator, your example is a compelling case for expression-based languages.

    • Basti Ortiz
      Basti OrtizSep 8, 2024

      You know, you make a very strong point. 😅

  • Erich Buri
    Erich BuriSep 8, 2024

    Here is an example where I use that operator on a regular basis - reduce!

    Array with objects that have a unique id reduced to a map, indexed by id:

    a.reduce((acc,item) => (acc[item.id]=item,acc),{})

    It’s more efficient than:
    a.reduce((acc,item)=>({…acc,[item.id]:item}),{})

    • Basti Ortiz
      Basti OrtizSep 8, 2024

      I do understand the appeal of one-liners, but how do you feel about this refactor?

      a.reduce((acc, item) => {
          acc[item.id] = item;
          return acc;
      });
      
      Enter fullscreen mode Exit fullscreen mode

      I personally prefer it this way—albeit a bit more verbose than yours. I'd love to know your thoughts on that.

    • Gergely Mészáros
      Gergely MészárosSep 11, 2024

      Your example is basically a map, therefore not very useful (not clean) to implement it with reduce. Just do: Object.fromEntries(a.map(x=>[x.id,x]))
      However valid point, I can imagine situations where it is useful.

      • Basti Ortiz
        Basti OrtizSep 11, 2024

        Oh, right! That is definitely much prettier than the reduce. Thanks for calling it out. 👏

  • awschult002
    awschult002Sep 8, 2024

    I use the ternary all of the time. Is one of my favorites for simplifying an expression.

    However, there is one scenario that comes up a lot that I haven't been able to held and that is conditionally executing a function that doesn't return a value, thus no final assignment.

    I would really like to see a clean ternary expression like you show, but without the need to assign a final value.

    • Basti Ortiz
      Basti OrtizSep 8, 2024

      I'd like to see a code example. Why wouldn't an if statement suffice?

  • Doncho Angelov
    Doncho AngelovSep 9, 2024

    It's a good article, in the sense that it describes how to write more cryptic code.

    As a C++ developer for more than 30 years, I've always prioritized reading and understanding the code over the shortness of the expression.

    How quick it is to read and understand the code is way more important than the shortness of the expression. And that's valid for more than 95% (number from the top of my head) of the production code, especially bearing in mind that the compilers are excellent in the optimization of "well written, but more descriptive than short" code.

    • Basti Ortiz
      Basti OrtizSep 9, 2024

      I understand where you're coming from, which is why I limited my usage of it to at most three inlined statements.

      Working within the context of conditional ternary expressions, let's consider the alternatives: (1) if statements with variable initialization far detached from the declaration and (2) ad hoc functions that encapsulate multiple statements.

      Frankly, I will always reject (1) because of how bug-prone it is as I've argued in the article. I'm willing to accept (2), but if one must write three-line ad hoc functions just to work around the comma operator, I think that's poor practice.

      And so considering all other alternatives, this is how I reached my conclusion in the article. I strongly disagree that it's "cryptic code" (especially when we cap it at three inlined statements) because the alternatives are either bug-prone as in (1) or cumbersome as in (2). The comma operator can get the job done clearly and concisely in this happy medium.

  • Mark David Robert McDonagh
    Mark David Robert McDonaghSep 11, 2024

    This feels like a corner case using a rarely used solution, and is only going to confuse other developers, especially juniors. This would likely lead to an organisation banning comma operators in their coding standards.

    • Basti Ortiz
      Basti OrtizSep 11, 2024

      I could see that being the case. Though I'd like to clarify that I never advocated for its use everywhere. I was careful to note that readable code must be tasteful.

      That's why my compromise is to only allow at most three inlined statements. That's what I've found to be the perfect balance between readability and bewilderment.

      I think outright banning the syntax is counter-productive when the pattern can effectively and concisely communicate the intent of the code. It's like forcing crutches on us.

      Instead of keeping ourselves away from these patterns, let's educate others (especially the juniors) on their proper and responsible use. Hence, I believe that banning the syntax only perpetuates the confusion around the comma operator. I find that unfortunate.

  • Harald
    HaraldSep 11, 2024

    In some languages, it's even possible to have some fun with the comma operator

  • Gergely Mészáros
    Gergely MészárosSep 11, 2024

    My biggest concern would be that the form (a,b) exactly looks like a tuple used in almost every other languages (and even in mathematics). Using something what looks like a tuple but works like a hidden side-effect might cause confusion.

    • Basti Ortiz
      Basti OrtizSep 11, 2024

      That's a totally fair argument. I will have to agree with you on this one. If there's any reason to contest the comma operator in C-like languages, this would be it.

  • Gergely Mészáros
    Gergely MészárosSep 11, 2024

    fun fact: (,) operator corresponds to flip const in Haskell. Unfortunately without partial application and lazy evaluation it has few true use cases. (Otherwise it has many).

  • Martin Baun
    Martin BaunSep 11, 2024

    The only thing I've used the comma operator for is writing a "clever" macro to track where a variable is getting used :P

    • Basti Ortiz
      Basti OrtizSep 11, 2024

      Some people at Twitter also had the same sentiment. The C macro ecosystem never fails to astound me. 😅

  • Chris S-D
    Chris S-DSep 30, 2024

    Simply speaking from experience here, I used to feel the same way about the comma operator.

    Unfortunately, experience has taught me that this particular usage has almost always ended up being confusing to other developers, and unfortunately, the comma is such a small operator, that it gets easily missed in reviews and devs that are scanning code can frequently miss important things going on unless they are being extra vigilant (which is mentally taxing). This ends up being more costly in terms of readability than it may seem.

    I get it, I love having code that is terse, and there are certain things that can make it this way that are much more easily identifiable, such as the double bang (!!) that forces a Boolean, and occasionally, but not quite as obvious, converting strings to numbers via the + operator (e.g. +'42'). Once understood, these patterns are much easier to interpret at a glance. Unfortunately, my experience with the comma operator has never turned out as favorable as the others. There's just something about it that isn't as recognizable at a glance.

    Perhaps if you could enforce a rule that using it for this has to be done only under certain circumstances like ternaries, maybe it would be more recognizable, but most devs don't read the rules, they just look at code and figure out what they can do by following patterns they see. This is one pattern that just isn't as obvious what you should and shouldn't use it for. I think you're going to find a lot of pushback from most Sr. devs if you try to use it.

  • Pierluigi Pesenti
    Pierluigi PesentiNov 21, 2024

    Besides the comma operator itself, and it being readable or not, there is a more important point to be made about ternaries.
    They are not a 1:1 alternative to an if/else statement. A ternary is an expression.
    And that's what makes it so different (and why not, powerful) from an if/else which is a statement.
    An expression should be referentially transparent and equal its evaluated value.

    In your example:
    const result = condition
    ? (doSomething(), value)
    : (doAnotherThing(), another);

    If doSomething() and doAnotherThing() are not returned, it implicitly means they are not pure functions (or getters). If they are not pure, they rely on things or do things without returning. Which means they are side-effectful.
    You should not put side effects in expressions as they loose their transparency.
    An expression relying on purity can be moved anywhere (even in a function signature as a default value for an argument) and they will not loose the referential transparency.

    In the example made by @buri with the reducer it is different as the only thing that happens is a mutation of a local variable (the accumulator).

Add comment