How about a new operator?

Borrowed from c#. Which means: unwrap_or_else

Personally I really miss it. C.unwrap_or_else(||5) Vs c ?? 5

Rust is generally strongly averse to adding new operators. Also, Rust's operator semantics are generally trait-based. What would a trait for the ?? operator look like and what would be its other use cases?

Note that, unlike C# and Java, Rust doesn't have the problem of pervasive nulls. Optional<T> should be a relatively rare occurrence in proper code. This means that optimizing purely for Optional is a bad idea, and a new feature should be useful for other types as well.

11 Likes

Seriously? Optional is rare? I already got annoyed by unwrap, some, they are everywhere. I mean look at GitHub, any serious web, db frameworks In rust. And result, which is really promoted by rust, since we don’t have throw/catch/finally, which I am still trying to wrap my head on. Not sure the advantage over try catch yet. Don’t get me wrong, option is great. C# has its own nullable type. But I personally think option is better. It is not just about null. It’s about what if anything go wrong, and I don’t care about the error, just give me an default value if anything goes wrong.

And the ? Operator, personally I think c# handles it better, but that’s debatable.

Let c = a?.b?.c ?? d:

Very smooth and clean. But this is impossible in rust, since kind of force the user to deal with errors by return immediately if I use ?.

Unfortunately, adding ?? as written would likely be a backwards-incompatible change, because today that would mean "apply the ? operator twice" (which is useful sometimes, if you have a Result<Result<T, E1>, E2>). It might theoretically be possible to distinguish in the parser on the basis of binary vs unary-postfix, but that would make for challenging parsing.

For the common case of wanting to diverge in the fallback, there's let Pattern(x) = c else { return ... };.

6 Likes

This is achievable with combinators. Not the easiest thing to write or read, admittedly. try expressions would be the proper Rustic solution, but they still have unresolved issues with type inference.

That really doesn't help writing bug-free software. But if you don't care, you can already use unwrap or unwrap_or_default on you Results. There is no reason to promote wrong code with syntactic sugar, the current solution is explicit and verbose just enough to be useful, but also obvious in its hackiness.

That strongly implies that you have issues with your data model. Can you link some example project? Most things shouldn't be optional to begin with, the cases which are (e.g. deserialization of rpc messages or CLI parameters) should generally handle optionals at the system boundary, by failing or substituting default values. The rest of the code should be able to assume that all values are well-formed.

The primary advantage is that you explicitly know which functions are non-fallible, which are fallible, and which errors are possible. You don't get woken up at 2 a.m. because you have forgotten to handle some rare edge case.

Another benefit is that fallibility becomes a first-class concept which can be dealt with in the usual way. For example, how do you deal with a fallible iterator in Java? You have to run the explicit for loop, and wrap each individual call in a try-catch. If you are trying to write generic code, then you're screwed. There is just no way to write a generic iterator adapter which can deal with the possibility of errors.

In Rust, you just have an iterator of Result<T, E>. You can use all the usual iterator methods on iterators of results. If you want to fail on the first error, you can use collect::<Result<_, E>>(). If you want to list all errors that happened, you can use collect::<Vec<Result<_, E>>(). If you want to write an adapter which works specifically on fallible iterators, like try_collect, you can easily do it.

There are also some benefits for the compiler, but these are more niche and not a clear-cut case.

6 Likes

My big gripe with let-else is that it doesn't help you in the other cases, where you don't want to diverge. I cannot return a default value. I cannot get a default value which requires an effectful (try, async, generators) computation.

When writing async code, I have constantly experienced the frustration from being unable to use the standard Option and Result combinators, because the computation in the closure is async. The case of diverging in the else branch is, imho, much more rare, outside of deep pattern matches like in the compiler or Clippy. Most cases are handled by ?, even more would be handled if Try was stabilized.

1 Like

Ah, fair enough. That's something we're looking at larger-scale solutions to solve (making it possible for functions taking closures to be "async if their closure is async"), but it's not at all easy.

1 Like

How about simply reuse ? Operator!

a? Will stay the same a?b will mean a unwrap_or_else b And it has to be short circuit.

The issue with combinators would be easily solved by making unwrap_or into a postfix macro. I see the issue is still open, after 4 years, and not closed or postponed, so I guess the desire to add them is still present. There is concern about name resolution and type-based dispatch. Is there anything else blocking the RFC?

Also, have you considered some more limited postfix-macro-like functionality, like Kotlin's inline and crossinline functions?

A major part of macros, particularly declarative macros, in Rust implement the functionality which is really syntactically valid Rust code with identifiers or function blocks sliced in. If those were reified into a kind of "template functions", obeying the usual privacy and name resolution rules, many issues of macros would just vanish.

Too special for my taste. The more general operator

macro_rules! unpack {
    ($e:expr, $variant:path, $otherwise:expr) => {
        match $e {$variant(value) => value, _ => $otherwise}
    }
}

with possible syntax

e.unpack(Variant) else {otherwise}

is already surpassed by the brevity of let-else. That otherwise is required to diverge is new to me, also too special for my taste.

Someone mentioned try already, but to elaborate, with the new desugar scheme I'm working on, that would be

let c = try { a?.b?.c }.unwrap_or(d);

(assuming d is not itself also an Option.)

I actually like that better than C#'s ?., because in C# that occasionally does something weird because it has a slightly-magic scope -- a?.b.Foo() and (a?.b).Foo() do something different, even though there's no precedence reason for the parens there. Having the block written directly in the form thus, I think, fits better for the Rust form.

But I would like to see Something for coalescing; aka generalized/improved `or_else`.

9 Likes

That's ambiguous: what does a ? -3 mean?

  1. a.unwrap_or_else(|| -3)
  2. try { a } - 3
5 Likes

I would say 2. is correct, and 1.need to be written a ? (-3), in other words, - is in priority.

[Feature Request] Operator on Try that is like .unwrap_or made this without seeing the discussion here. But it is in a slightly different vein.