Proposal: deprecate ternary

This is just an idea; feedback welcome.

Ternary is historically in the syntax purely for familiarity, but we might be able to remove that now:

  • It’s needed in JS for expressions, e.g. cond ? <div /> : <span />. Our if is already an expression, and is slightly lighter than JS’ if thanks to absence of parentheses. Compare
    cond ? someStuffHere() : moreStuffHere()
    
    vs
    if cond {someStuffHere()} else {moreStuffHere()}
    
    The former is still more concise but then the benefit might not justify the feature.
  • It might be used as an ad-hoc pattern matching-like syntax in JS:
    a ?
    : b ?
    : c ?
    : d
    
    This isn’t really that readable unless you’ve assimilated the pattern; we have actual pattern matching already.
  • For React, seeing a large component then suddenly seeing a ? many lines later isn’t great for readability. Seeing an if upfront sets the right mindset.
  • Smaller syntax space, less misleading grammar highlighting, other misc benefits.

The transition path would be to print ternary into if, then remove it at a later date.

Again, feedback welcome! Understandably some shorter pieces of code would become less concise, but we’d like to see them and weight the tradeoffs (real world code only please).

2 Likes

If ternary is removed, could the formatter preserve short if blocks on single lines?

let a = <p className={foo ? "bar" : "baz"} />
let b = <p className={if foo { "bar" } else { "baz" }} />
let c =
  <p
    className={if foo {
      "bar"
    } else {
      "baz"
    }}
  />

In the above code, either a or b are fine with me. c looks like a big regression, though, and it’s how the formatter currently writes if.

I tend to agree that ternary should be avoided in general for readability, but single-line statements are the exception IMO.

4 Likes

I think discouraging ternary is fine, but it seems harmless to support the syntax for a long time (aka years, if not indefinitely) since it’s native to JS. No reason to make it more difficult to onboard ternary-loving JS devs.

My main use cases for ternary are in variable assignment, template literals, and conditional JSX.

In general, it seems like pointing people who like ternaries towards true/false switch syntax might be an even better idea than if

Here’s a playground with contrived examples of those use cases comparing ternary, if/else, and switch syntax

The template literal use case is the only one that seems broken without ternary, but to be fair, it’s not the cleanest even with ternary…

I’m not a fan of ternaries by an means, and I believe it’s an important thing for the syntax to support for familiarity for developers coming from JS, especially in JSX and template strings. My gut feeling is that the less surprising a new syntax (meaning ReScript vs JS) is, the more likely it is to be adopted. I do think “thinking in ReScript” should be encouraged by the compiler via warnings and such – perhaps this can be scoped to nested ternaries? Having new developers able to write code that looks mostly like JS, then being given compiler guidance on how to write more ReScript-y code I think will prove more beneficial in the long run than removing the ternary syntax entirely.

There is also the option of providing the “auto-refactoring” options in the VSCode extension, and have a compiler warning for nested ternaries prompt to auto-refactor to pattern matching or if-else.

I’d say that ternaries are so widely spread and often more readable (e.g. bottom) that I’d go for keeping them.

let x = a ? 1 : 0
// vs
let x = if a { 1 } else { 0 }
10 Likes

To me, doing this for the sake of simplicity would be a bridge too far. Rust removed it, but Swift retained it.

A lot of existing code would have to be re-worded to make it format better.

An alternative is you could just heavily de-emphasise it in the docs, and leave it to user discretion.

2 Likes

I’d say line 31 mostly suffers from bad syntax hl. Otherwise, it’s more readable than its brethren.

1 Like

I also think removing it is a bit much, especially considering it’s so widely used in JS. But getting rid of it from the docs (or heavily deemphasizing it) sounds like a good idea. Maybe even blocking it from being nested.

2 Likes

Looks like a crazy idea to me.
Sure long ternary are bad for readability but like all languages and tools, you can do great things or bad things with it.
I use lot’s of simple ternary and would be really pissed of if this operator is removed.

2 Likes

Ternaries are so widely used in JS that not having them seems like it will alienate incoming JS users?

I hate long complicated ternaries, but sometimes they are just a nice elegant way to represent small conditions e.g. String.length(value) === 0 ? None : Some(value)

What if for single line ternary-style if expressions the brackets were not required? if value->String.length === 0 None else Some(value) is still very short. Although not having any syntax between === 0 and None might make it harder to follow than a ternary.

1 Like

Yes

Funny, that’s what ternary deguars to! Though I think since most of the benefit is in the brevity, the raw switch doesn’t seem to accomplish the task.

However like johnj asked, we’d format such if-else onto the same line. So your snippet with if-else isn’t nearly as bad within template literal: updated.

I’m thinking maybe there’s a nice sweet spot between allowing the ternary syntax, but formatting it to if-else?

Hey I insisted on real world code please =). The problem with theoretical snippets like a ? 1 : 0 is that the character count is dominated by the if-else keywords and braces. In reality, 95% of your code using ternaries aren’t using single character condition, consequent and alternative. We don’t want to accidentally represent the status quo as prettier than it actually is.

That said, I’m interested in seeing your actual usage patterns in your codebases.

I’m not too sure we’d like to compare ourselves to Swift in terms of language complexity, and the fact that even Rust (a language that’s already very complex) removed it does speak volume.

To be clear, you won’t have to manually reword them; the formatter takes care of it. What I’m wondering right now is whether formatting it to a single-line if-else is good enough or not.

A bit of a tangent but, we don’t believe in de-emphasizing such a feature. If it exists it should be documented, and if it’s not documented then it doesn’t exist. We’d rather explicitly document and discourage a pattern rather than discouraging-by-obscurity (though sometime due to the lack of time and out of politeness vs some userland libraries, we do end up doing so).

Maybe…

Please chill a little these days… we value production users’ opinions, but you’re bringing the same argument without examples and with a flamey tone here and elsewhere.

Good point; I forgot to write this down; the deal breaker here in terms of brevity is indeed the braces, though we can’t remove them because of ambiguities: if display if present log() else warn(). Where does the else belong, etc.

1 Like

Would not requiring curly braces for the two branches be a compromise?

Edit: probably hard to make that unambiguous.

Edit2: Elixir has special syntax for an if one liner if (actually it’s generic); if <condition>: a, else: b

See right above =P

Hummm I think I’d rather preserve ternary for this than deprecating ternary and adding that syntax.

2 Likes

How about scala syntax?

if (i == 1) x else y

This has the advantage of being clearly different to regular if blocks, where the brackets are removed.

Since we’re talking about ternary style code, each if is guaranteed to have an else. Perhaps that can be a condition of removing the braces? The above would then format to if display { if present log() else warn() }.

1 Like

What happens to if (i == 1) if (j == 2) x else y? Assuming x and y can be unit

1 Like

This else branch belongs to second if in Scala. but tbh this leads to ambigiuos/complex code.

I prefer not having ternary removed. It makes sense to have them, they are sometimes more concise than if expressions and people coming from js will find a direct translation.

Maybe we can do something about complex ternaries.

  • Warn/discourage complex ternaries in documentation or in a separate official style guide.
  • Print complex ternaries (long/nested) into if expressions so when people use formatter printer will purge complex ternaries away.
3 Likes

Assuming my earlier statement about “ternary style code requires an else”, I would parse it assuming the innermost if is the ternary.

if i == 1 {
  if (j == 2) x else y
}

This does bring us back to whether nested ternaries should be allowed, though, because a valid nested ternary in this style would be if (i == 1) if (j == 2) x else y else z. Which is about as difficult to follow as i == 1 ? i == 2 ? x : y : z without multi line formatting.

2 Likes

Personally I’d rather we remove the if statement than the ternary operator. Especially if we’re trying to guide people to use switch properly to handle all use-cases.

This is a big reason why I’d want a ternary over an if statement. A ternary is a mini-switch for a boolean value, forcing the developer to handle all possible cases. Encouraging if statements to be used more often can result in more scenarios where possible states for an application go unhandled (preventing that is a big reason for why I chose ReScript).

Here’s some real-life ternary usage from the project I’m currently working on.

      <Framer.Div
        className={"fixed top-0 bottom-0 left-0 right-0 z-2000 text-base" ++ (
          visible ? "" : " no-interaction"
        )}
        initial={"background": "rgba(0,0,0,0)"}
        animate={"background": visible ? "rgba(0,0,0,0.2)" : "rgba(0,0,0,0)"}
        onClick=closeHandler
        ref={ReactDOM.Ref.domRef(refBackground)}
      />

In the above I wouldn’t want to lose the ternary operator.

That same component a few lines later does the following, to which I’ll agree that it can probably be refactored to a switch case, though I’d say it’s up for debate on whether that’s easier to parse.

        {visible
          ? <OpenSocial.Components.Molecules.Tray className={"text-base max-w-lg z-2000 bg-white flex flex-column"}>
              <ChatWindow closeChat={_ => setVisible(_ => false)} />
            </OpenSocial.Components.Molecules.Tray>
          : React.null}

This is what I’d reformat that as with a switch

        {switch (visible) {
          | true => <OpenSocial.Components.Molecules.Tray className={"text-base max-w-lg z-2000 bg-white flex flex-column"}>
                      <ChatWindow closeChat={_ => setVisible(_ => false)} />
                    </OpenSocial.Components.Molecules.Tray>
          | false => React.null
        }}

With switch statements and JSX components I always struggle with the indentation, since I want to have my opening and closing tags on the same indentation level for easiest readability.

The alternative indentation would be the following which I dislike for disconnecting the true condition from the component. Although I suppose I could get used to it.

        {switch (visible) {
          | true =>
              <OpenSocial.Components.Molecules.Tray className={"text-base max-w-lg z-2000 bg-white flex flex-column"}>
                <ChatWindow closeChat={_ => setVisible(_ => false)} />
              </OpenSocial.Components.Molecules.Tray>
          | false => React.null
        }}

To round off two other examples of a ternary usage in the code-base.

let className = "overflow-auto flex" ++ (reverse ? " flex-column-reverse " : " ") ++ className->Belt.Option.getWithDefault("");
let getLastReadDisplay = (conversation) => {
  open Belt.Option;
  conversation
    ->getLastRead
    ->flatMap(lastRead => conversation->hasMessagesSince(lastRead) ? Some(lastRead) : None)
}

All-in all, a ternary operator is just a really useful way to do a bool -> x type transformation.

1 Like

I do have that kind of ternaries in my app :smile:

let count =
  1 + (redactedA > 0 ? 1 : 0) + (redactedB > 0 ? 1 : 0)
<CallToActionLink
  href={link.href ++ (queryString === "" ? "" : "?" ++ queryString)}
  title={link.title}
/>
1 Like

using keywords instead of braces makes it a bit more readable on a single line. And it works at multiline too. Downside is that this introduces another keyword (I think might not be necessary with a handwritten parser). Another downside is that it’s definitely less JavaScript like.

let a = if bla == 0 then 0 else 1 end
let a = if bla == 0 then
  aLongerExample()
else 
  aLongerExample2()
end