Why can't I use the `?` operator in this way?

fn why_cant_i_do_this(some_map:HashMap<usize, Vec<usize>>, some_key:usize) {
    let some_val =
        {
            let some_vec = some_map.get(&some_key)?;
            some_vec.pop()
        };  
    println!("{:?}", some_val);
}

Apparently the entire function has to return an Option

fn but_this_is_allowed(some_map:HashMap<usize, Vec<usize>>, some_key:usize) -> Option<usize> {
    let some_val =
        {
            let some_vec = some_map.get(&some_key)?;
            some_vec.pop()
        };  
    println!("{:?}", some_val);
    some_val
}

Is this limitation real or am I missing something?

1 Like

? bubbles up to function boundaries. You can use an IIFE-style closure to define the boundary:

let some_val = (|| {
    let some_vec = some_map.get_mut(&some_key)?;
    Some(some_vec.pop())
})();

On nightly with the try_blocks feature (tracking issue), you can do

let some_val: Option<usize> = try {
    let some_vec = some_map.get_mut(&some_key)?;
    some_vec.pop()?
};

instead.

Though in this specific case you can avoid all this with an and_then:

let some_val = some_map.get_mut(&some_key).and_then(Vec::pop);
2 Likes

The ? operator applied to some_map.get(&some_key) is just shorthand for the following match block.

match some_map.get(&some_key) {
    Some(i) => i,
    None => {return None;},
}

Notice that the second match arm returns a None, and terminates the execution of the function, which means the functions returns an Option. Your first function has a return type of () (i.e. no return values), which means you can't return a None, which is why ? doesn't work.

1 Like

What you may be missing is that the hasmap.get() returns an Option. Some or None.
That "?" is going to do a return if there are None. Returning the hashmap.get() result, an Option containing None.
Therefore the function has to return the same thing, an Option.

This might not be what you want of course. You might want to do a match on that hashmap.get result and do something different.

I get it; I was hoping that the ? operator behaves like the usual monad sugar. For example, in F#

let why_cant_i_do_this (some_map:Map<int, int list>) some_key = 
    let some_val = maybe{
        let! some_vec = some_map.TryFind some_key
        return! some_vec |> List.tryHead
    }
    printfn "%A" some_val

is just syntactic sugar for

let but_this_is_allowed (some_map:Map<int, int list>) some_key = 
    let some_val =
        some_map.TryFind some_key
        |> Option.bind List.tryHead

    printfn "%A" some_val

In some functional languages with some support for monads (such as Haskel and F#) a sugaring is provided for the bind function (which is part of the monad). In these languages the bind function is typically called bind. In Rust this function (at least for the option type) is called and_then.

I am surprised that the monad sugar (?) is not as flexible as it can be.

But I am not returning a None. The None gets assigned to some_val. This limitation of ? seems a bit arbitrary.

Classical exceptions behave a bit more consistent in this regard. You can catch an exception at Block boundaries and not function boundaries.

I have no idea what you are talking about. That is not your fault, nobody has ever successfully managed to explain to me what a "monad" is or why I might need one. I guess I'm a bit slow.

Makes me very happy that Rust is no more "functional" than it is.

The semantics you seem to refer to are expressed in Rust (nightly-only for now) as try blocks, as was stated before.

I don't think so.

You have a "?" there. Which means some_vec will only see "Some". If there is "None" then the "?" causes a return.

If you want some_vec to get the "None" as well as "Some" then some_val will itself be an Option and you have to do a "match" on it to see if you have "Some" or "None".

Your "some_val" is getting the result of some_vec.pop(). Only if some_map.get returns Some.

Lol. I think you intuitively understand what a monad is, but don't realize it.

Loosely speaking, a monad is just a wrapper type of some sort, and a bind function. There is more detail to make it rigorous but it is not important right now.

Option<T> and and_then are a monad. You have your wrapper type (Option) and a function that takes an unwrapped value (T) and creates a new wrapped value (Option<S>)

There are lots of things that have this pattern. Result is an obvious one.

The idea is that some_val is an Option<usize> itself. If some_map.get returns None then some_val would also be None. As mentioned earlier, this functionality is achieved with the upcoming try blocks.

Coincidentally only today I was wondering what use case there is for "?".

The scenario:

After a few days hacking I finally had a relatively simple program working in Rust. After a long struggle trying to understand what tokio is up to.

My program ended up with a lot of "?" scattered around. Mostly because that is what I found in various example codes around the net.

That is great and all. No need to do lot's of testing on return values all over the place.

BUT, the end result is that when an error occurs, can't bind to a socket, trying to write to a serial port that has gone away, etc then that error percolates back up the call stack and the program dies with some generic error message.

Those error messages were not very useful. I want to know what serial port failed for example. I had to go through the whole program and catch errors where they happened to sort this out.

Or am I missing a point here?

Wait, so what do you want to behaviour to be when using ??

Try blocks act as a closure; they return an Option/Result:

let x: Option<usize> = try {
    Some(3)?
};

x is still an Option, and the ? operator just broke out of the try block instead of the most local function.

Additionally, if you want this to work:

fn foo() { // -> () implicitly
    None?;
    println!("Hello, World");
}

And print Hello, World, then what would be needed is to implement From<NoneError> for ().

The behaviour of ? on a type is dictated by std::ops::Try, which when used on an Option, its Err type is NoneError.

As stated in the docs:

The error type that results from applying the try operator ( ? ) to a None value. If you wish to allow x? (where x is an Option<T> ) to be converted into your error type, you can implement impl From<NoneError> for YourErrorType . In that case, x? within a function that returns Result<_, YourErrorType> will translate a None value into an Err result.

Hence, if you want to be able to use ? on a None variant to return early out of a () returning function, std would have to add an implementation for From<NoneError> for ().

Unfortunately, that might cause even more ambiguity errors:

fn foo() -> Option<()> {
    (|| None?)()
}

Works fine, however lets introduce a mock Unit type to replace () upon which we can implement our own traits:

#![feature(try_trait)]

use std::option::NoneError;

struct Unit;

impl From<NoneError> for Unit {
    fn from(x: NoneError) -> Self {
        Unit
    }
}

fn foo() -> Unit {
    (|| None?)()?;
    
    Unit
}

However, even this produces an error:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`)
  --> src/main.rs:14:5
   |
13 | / fn foo() -> Unit {
14 | |     (|| None?)()?;
   | |     ^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `Unit`
15 | |     
16 | |     Unit
17 | | }
   | |_- this function should return `Result` or `Option` to accept `?`
   |
   = help: the trait `std::ops::Try` is not implemented for `Unit`
   = note: required by `std::ops::Try::from_error`

Which points out that the function's return type must also implement Try.

Usually for situations like these, you need to write your own error types implementing the Error trait, as well as the From trait for the various kinds of error you can expect to see in your function. In the end, it should end up returning your error type, with an enum variant describing what actually went wrong. If you don't want to think too much about this, the anyhow crate simplifies this process.

This pattern was popularized by the guy who made this video.

He also recently made a guide on when not to use it.

If I understand correctly, OP is looking for a way to use the "return-early" property of the question mark operator on block boundaries (instead of function boundaries, as it works now).

Yes exactly.

Ah, okay, so in the following program, you'd expect "Okay" to still print:

fn foo() -> Option<()> {
    {
        None?;
    }
    println!("Okay");
}

Where you'd have to bubble up errors like the following to get today's behaviour:

fn foo() -> Option<()> {
    {
        None?;
    }?;
    println!("Okay");
}

Not sure about that function. Why does it return an Option?
The use case is more something like this

fn foo() {
    let some_value_i_dont_care_about_i_only_want_the_side_effect = {
        let a = some_opt_func1()?;
        let b = some_opt_func2(b)?;
        let c = some_opt_func3(c)?;
        do_something_with(c)
    }
    printfn!("the side effect was done only if a chain of options were all Some")
}

turns out this will be doable using try