Ternary operator (maybe a rehash)

I know that rust doesn't implement/support the C-style ternary operator:

let one_if_positive: isize = (x > 0) ? 1 : 0;

and instead uses a more general

let one_if_positive: isize = if x > 0 {
       1
   } else {
       0
   };

Rust is lovely, but this for me is a syntactic papercut. It bugs me as one of the things I like about the rust environment is rustfmt, and I like rustfmt as it fits my philosophy (ok, maybe prejudice: mostly k&r / 1tbs). This breaks that fit since it forces five lines (after rustfmt has had its way) instead of one where the philosophy.etc is "minimise vertical real-estate consumption", and the C construct allows this to be a one-liner (as above) or a three-liner:

let one_if_positive = (x > 0)
    ? 1
    : 0;

So am I just being crabby? I know that k&r/1tbs mandate what rustfmt does, so I can't complain about that, but the ternary op allows a more compact statement where it's feasible.

Is there some deeper reason why the ternary construct would create issues with parsing (and is therefore not provided)?

I have seen on a casual goggling macro solutions, but have not investigated them. I am specifically asking why this isn't part of the 'out of the box' toolkit).

I would argue that in an expression-based language, the ternary operator is unnecessary syntax sugar, and for nested branching it's also definitely cleaner to use if... else. So, the benefits of introducing the ternary operator are not big enough to deem the change.

13 Likes

Here is the RFC Rust Needs The Ternary Conditional Operator (-?-:-) · Issue #1362 · rust-lang/rfcs · GitHub

1 Like

I believe the reason Rust doesn't have a separate ternary operator is that the if-else construct already serves a similar semantic role. The language designers likely chose to avoid introducing redundant syntax with overlapping functionality. From a syntax parsing perspective, I don't see any significant issues with adding a ternary operator, it appears to be purely a design decision.

You can experiment with Rustfmt's settings, such as single_line_if_else_max_width. While this won't be as concise as a C-style ternary operator, it allows you to enforce inline formatting in certain cases.

8 Likes

Yep. Yandros has a pretty good post about just that.

1 Like

I love the ternary operator in other languages. But I would vote against adding it to Rust.

  • Adding the ternary operator would result in 'duplicating' the if/else behaviour, which would make the language more 'complicated' due to overlapping.
  • if/else is more readable. I agree a ternary operator looks more elegant when you're used to it, but is not necessarily more readable. Someone not knowing code can understand what if/else might mean, a symbol like ? looks cryptic.
  • I like Rust does not allow you to use 'shortcuts' like this but forces you to write more readable and explicit code.

TL;DR -- I like the ternary operator, but not for Rust. It does not match the philosophy.

7 Likes

You can configure rustfmt not to force line breaking in some situations, if you prefer the compactness, giving

let one_if_positive: isize = if x > 0 { 1 } else { 0 };

Even if you can't, you can always disable rustfmt for that one expression with #[rustfmt::skip].

You could also abuse coercions:

let one_if_positive: isize = (x > 0) as isize;

Though, personally, I think that's too clever by half and would probably advocate against doing so for readability reasons.

1 Like

Ternary ? : would also introduce parsing ambiguity requiring lookahead since we have ? as the Try operator, especially when combined with other unary operators. For example, seeing condition ? & expression... that could be a bitwise AND, (condition?) & (expression), or it could be the ternary (condition) ? (&expression) if the next token is : and an "else" expression.

10 Likes

The main time I miss it is when chained. I don't think it pulls its weight on the whole,[1] similar to the other comments, but it is a bummer in those niche circumstances.

A compact alternative to a long if-else chain with short conditionals and blocks is:

    let result = match () {
        _ if a => 0,
        _ if b => 1,
        _ if c => 2,
        _ if d => 3,
        _ if e => 4,
        _ => 5,
    };

Though some may disagree, I find that better than

    let result = a
        .then(|| 0)
        .or_else(|| b.then(|| 1))
        .or_else(|| c.then(|| 2))
        .or_else(|| d.then(|| 3))
        .or_else(|| e.then(|| 4))
        .unwrap_or_else(|| 5);

And I have also been known to

// (too long to just be a one-liner)
match cond {
    true => expr1,
    false => expr2,
}

That saves one line -- which I don't generally care about -- but also avoids the jagged left side shape and avoids separating the expressions. I.e. it can improve readability IMO and isn't just an "optimizing for writing" technique.


Syntactically, I don't think it could be cond ? ex1 : ex2. Maybe with arbitrary look ahead? But this is valid Rust today for example.

let x = variable ? -func() + or_whatever * long_expression;

That said, I also don't believe we'll every get the operator, so the exact syntax is moot; only throwing this out there due to the comments about that exact syntax being viable.


  1. mainly because it's too easy to abuse ↩︎

3 Likes

Perhaps I'm weird but as a user of C since 1983 who still has affection for the language the ternary operator has always been a syntactic paper cut for me. It's redundant and it's prone to misuse and confusion. My other C paper cut is that one can have a single statement after if and friends without any braces around them. Also a redundant feature prone to misuse and confusion.

All in all I vote for not introducing ternary operator to Rust.

2 Likes

Quite likely. The x ? a : b syntax might require two characters while if x { a } else { b } needs ten: the latter is infinitely more readable and obvious at a glance, however. Code golf is better reserved for languages and/or exercises and/or folks who have little else to do anyway.

You might have seen things like:

// C-like ternary operator
macro_rules! is {
    ($cond:expr; $if:expr; $else:expr) => {
        if $cond { $if } else { $else }
    };
}
// to be used as 
is!(1 > 0; "true"; "false");

You can make them arbitrarily complex and/or nested as well.

not recommended
// three levels deep
macro_rules! is {
    ($cond:expr; $if:expr; $else:expr) => {
        if $cond { $if } else { $else }
    };
    ($cond:expr, if $($if:expr);+, else $($else:expr);+ $(,)?) => {
        if $cond { is!($($if);+) } else { is!($($else);+) }
    };
    (
        $cond:expr, 
        if => $if_cond:expr, 
            if $($if_if:expr);+, 
            else $($if_else:expr);+ ,
        else => $else_cond:expr, 
            if $($else_if:expr);+, 
            else $($else_else:expr);+ $(,)?
    ) => {
        if $cond { is!($if_cond, if $($if_if);+, else $($if_else);+) } 
        else { is!($else_cond, if $($else_if);+, else $($else_else);+) }
    };
}
// to be (never) used as
is!(1 > 0; "true"; "false");
is![
    "is_nested".is_ascii(),
    if 10 > 5; "more"; "less",
    else 10 < 25; "less"; "more"
];
is!{
    "silly" == "silly",
    if => "why?" != "why?",
        if 10 > 5; "more"; "less",
        else 10 < 25; "less"; "more",
    else => "just" == "don't",
        if 10 > 5; "more"; "less",
        else 10 < 25; "less"; "more",
};

Though I would think twice (to the eighth power) before using it.

2 Likes

I guess configuring rustfmt would be a thing for me now, since I'm not coding in a group environment. I'd prefer if this was 'out of the box', if only since in a group environment where rustfmt default is no longer taken for granted, the feuds about how where to put the curly braces would become feuds about how to configure rustfmt to format the curly braces, which doesn't seem much of an improvement. It's an idea, though.

I found these links on a casual search:

'Does anyone use a customized configuration for rustfmt? - #11 by matklad'

"Reddit - The heart of the internet"

Looking for a good example of a rustfmt file Reddit - The heart of the internet

The discussion on Reddit has a top comment to the effect that the best rustfmt.toml is empty (which overall would be my view as well). As noted, it's the gratuitous use of vertical real-estate that annoys me about this; a ternary operator could have alternate rules.

I did see a couple of the posts that pointed out that this would in fact complicate parsing today:

let foo: i32 = my_func() ? 42 : -1;

looks initially to the parser as a try construct; then after choking on the colon it would have to go back and try it as a ternary if. That seems an argument for the status quo. There does also seem some history to this.

The syntactic ambiguity that may not get resolved until much later, plus what seems to be past (way in the past) back and forth on this makes sense as a reason to not support it.

I will look into either rustfmt.toml, or a macro. I suspect I can see a way to use it in a shared environment (where I'd agree that the the standard ought to be "rustfmt with all default behaviours" as being a strength of the ecosystem).

I'm going to risk going out on another limb by observing that the C world prohibition on changing syntax with macros like:

#define then {
#define endif }

is maybe not as bad in Rust where that kind of abuse isn't (as far as I can see) possible. So a macro (named one_or_the_other, either, ternary, suggestions welcome as to naming) might be acceptable, perhaps even as a 'house standard' extension...?

Thanks, that would solve my current problem.

In my own toy / experiments, I'd probably do this. In a shared/corporate environment I'd be hesitant: I've been to the curlybrace wars, and I see "rustfmt out of the box is definitive" as a strength of the Rust environment and ecosystem. I'd hoped that a ternary would provide an alternative that rustfmt could treat as more compact.

I wonder if that kind of prohibition was ever actually enforced. At least one very-well known product used macros that looked almost exactly like your then and endif (except, of course, fi wasn't just }, but ;} – for obvious reason).

Heh, yes. I reckon it's not a 'prohibition' as such, but a reaction to the mess that those macros leave behind.

I wish the if expression was spelled like this:

condition then { a } else { b }

This would more naturally flow in left-to-right evaluation order, similar to method call chains. First the condition is evaluated, then the decision is made.

1 Like
    const fn map<T>(self: bool, a: T, b: T) -> T {
        if self {
            a
        } else {
            b
        }
    }

It would be quite trivial to add something like the above to the bool impl, which would make it possible to write concise ternary expressions without needing complicated parser logic that a C-style ternary operator would.

1 Like

Combining with @tczajka comment, something like this is already possible, though rather unwieldy:

(condition expression).then_some(a).unwrap_or(b);

Do note that neither of those alternatives use lazy evaluation; for that you'd need to use the closure forms, . then and .unwrap_or_else.