The `?` operator and a `Carrier` trait

As far as my intent as RFC author: No. The meaning of the trait is literally "is isomorphic to Result<T, E> for some choice of T and E". That's it. Nothing more or less.

Any potential relationship is only a consequence of the fact that Result itself "is" a particular monad (although its type parameters are in the wrong order for defining an impl the same way as in Haskell).

(Also I think the From/Into on the error type would get in the way of trying to do any explicitly monad-y things.)

I’m a bit worried about the tendency to abstract away everything and everywhere. Sure, if you designed it yourself, everything is obvious and clear and maximally flexible, but for others it’s quite hard to understand what’s actually happening, especially for newcomers.

One example is the usage of From... and Into... traits in function signatures. E.g. function taking a FromIterator parameter, which again takes an IntoIterator.

My fear is, that if you now also abstracting the common return types, you end up with completely generic functions without any concrete types, that are incredibly difficult to “parse”.

One thing I like about Rust is the “explicit over implicit” policy, please don’t lose it.

4 Likes

I was against this entire feature on two counts:

  1. The syntax - IMO ? isn’t something people would intuitivly associate specifically with error handling and I think both ? and ! are significant punctuation signs that could have been better utilized in Rust.
  2. Semantics - this is of course the main thing - I’m against adding ad-hoc special case syntax just for error handling. With that I agree whole-heartedly with @withoutboats above.

@glaebhoerl regarless of intent, there is relation to monads here becuase Result is a kind of monad. I think that this is the aspect we need to concentrate on. You once mentioned a very neat insight - Rust allows procedural code so in essence Rust code is already inside a sort of ambient Monad. This is why the alternative to this design - do-notation - had objections. How do we deal with control flow such as break, continue, loops, etc inside the do-notation?

This insight is key - do-notation wraps pure functional code into a monad but we are already inside an ambient monad so we actually need the reverse operator, an unwrap operator. This is what the ? should be IMO. This also has precedent in other languages (swift has something like that, i think).

This also relates to my other interest, stateless coroutines as proposed by Gor Nishanov for C++. In fact, I have posted here before a link to one of his presentations that shows that his await operator has the semantics of such an unwrap operator. More precisely:

fn f() -> M<T> {...}

fn g() {
    let x = await f();
    do_something_else(x);
}

await unwraps the T value out of the container M. If this operation “fails” than the control-flow is returned to the caller.

I think that Rust’s ? should be the exact same await, and the carrier trait is already almost the same as Gor’s Awaitable concept.

@yigal100 The try!/? operator itself is monadic in some way, not so much the Carrier/ResultCarrier trait (which my previous comment was about). :slight_smile:

It's not completely clear to me -- are you saying ? is currently not such an "unwrap operator"? (If so, what's the difference? It sounds very similar from your description.)

See https://github.com/boostcon/cppnow_presentations_2015/raw/master/files/CppNow2015_Coroutines_in_C++17.pdf (especially page 52) for what I was talking about.

I wasn’t precise enough with my words - you are indeed correct that ? is the unwrapping operator, in a way. The problem is that currently it is intended to be a specific unwrap op just for Result instead of the more general purpose op I want which incidentally is also relevant for implementing [stackless] coroutines.

At least the design proposed by Gor is such that the Awaitable concept is an extension point for library authors and its implmentors such as Future<T> define what happens when await/return are called.

I haven’t yet figured out how to exactly translate the C++ into rust traits yet.

My expectation would be that ? and the monad bind operation would behave in a consistent manner between all types which implement Carrier, which is why it seems like there should be some relationship between Carrier and Monad, maybe a generic impl of Monad for terms which construct Carriers (which is why possibly Carrier should be #[fundamental]).

All the types that we want `? to support with the Carrier trait are in fact Monad instances (Result, Option, Future, etc…) so there already is a strong correlation. I think that either Carrier is in fact Monad or it should be an extension thereof.

This is kind of a side issue, but perhaps important. Making Vec<T> deref to [T] wasn't just about ergonomics: going from Vec<T> to [T], in my view, is dereferencing a pointer (just a fat one, that points to multiple adjacent items). That is, it is completely valid to think of Box<T> as a "vector always of length 1".

These are interesting points. More broadly, the point is that the design of the carrier trait itself is sort of non-trivial and contains various design questions (some of which may perhaps interact with other language features). Hence it may make sense to start out with an (amendment) RFC so that we can discuss those points (as well as the overall desirability of the future) before landing changes that would be harder to back away from.

Thinking a bit meta, I think we're still hammering out just what is the process for "answering" unresolved questions. My feeling is that there are many smaller questions where it's fine to hammer out an answer informally or through code and just revisit the question in the stabilization process. But sometimes there are also bigger questions, where non-trivial design is involved or which are particularly controversial, where it may make sense to come up with an RFC first before landing a PR.

After reading through this thread, I feel that we should gain experience with ? just on Result for now (and implement catch). If it turns out to be something we want to keep (which I imagine it will), I think we should expand it to be a general flow-control operator that can be overloaded through a generic (i.e., not bound specifically to the error use case) trait in std::ops. I feel like the introduction of such a trait warrants another RFC.

3 Likes

I am experimenting with the ? syntax in Dyon. See design.

In Dyon, the ? operator converts Option to Result with an error message that Some(_) was expected.

1 Like

I don't see any reason that the ? operator should be conceptually tied to “error handling,” although it's obviously useful for that. The same control flow that is useful in error handling is also useful in other code, and code that acts the same should look the same.

Rust's ? operator is directly inspired by other languages, notably C#, which uses it for null chaining, and Swift, which uses it for optional chaining (although I believe similar syntax for Rust was also proposed before Swift was released). Allowing it to be used on Option in Rust is not only internally consistent; it also has a learnability/consistency benefit for new users who have encountered the concept elsewhere.

2 Likes

But Rust's ? uses early return, which is quite different to both C# and Swift, where the null/nil values are propagated instead.

That said, I really don't like the ? operator that much in Rust, it has bitten me already a few times where I just overlooked it. The chaining is nice, but otherwise I prefer the try! macro, it's just more visible. If it could use method syntax, it would be perfect. Has anyone already explored macros with method syntax? For example with prefix ! instead of . like !check()

Check out TryInto.

Note: given the confusion around whether this change would require an RFC amendment (rather than just merging the PR experimentally), the core team met to determine the process. The result is here, with TL;DR that this will require a (small) RFC amendment to land. It’s not clear whether discussion should try to reach some consensus here prior to a new RFC thread, or whether we should open a thread right away and move discussion over there.

Sorry for the process delay, but especially given the contentiousness of the original RFC, we should err on the side of caution.

1 Like

The signature of slice::binary_search doesn't quite fit this. The Err variant says that the item was not found, and encodes where the item could be inserted. This is not indicative of an error. From reading more of this thread, it sounds like many would consider this API to be a misuse of Result.

5 Likes

I have been using ? quite a bit now on nightly (since I had to go nightly anyways for serde) and I miss ? not being able to propagate options somehow. My code is currently littered with or_ok()? calls. I think the boundary between Option and Result is somewhere and it’s definitely not error vs non error. It’s more payload vs absence of payload.

2 Likes

Also one thing I wanted to mention is that in other languages ? is exclusively working with Option (or nullables) so the assumption a programmer from elsewhere would have is that Option could be handled.

1 Like

AFAICS most of the inconvenience stems from the fact that rust container types use a Option rather than a Result. One idea is to introduce an official IndexError type.

fn foo(xs: &[u32]) -> Result<u32, IndexError> {
    let x = xs.get_or_err(1)?;
    let y = xs.get_or_err(2)?;
    Ok(x+y)
}

I feel like this function shouldn’t use a result but define a custom enum for this approach. Would look much cleaner.