Is there a reason compiler doesn't catch the possible "None" Option variant when passing a value to a field the type of which is known?

As the title says, my question is why doesn't the compiler complain about a possible "None" variant of the Option type when it is passed as a value to the enum field that expects an i32 value?

enum AmazingEnum {
    V1,
    V2 { something: i32 },
}

impl AmazingEnum {
    fn do_something(some_arg: &AmazingEnum) {
        match some_arg {
            AmazingEnum::V2 { something, .. } => {
                println!("Matched V2, this is a \"something\" field: {something}")
            }
            AmazingEnum::V1 => (),
        }
    }
}

fn main() {
    // deliberately specifying as "None" for the sake of example:
    let another_value: Option<i32> = None;

    // this compiles just fine
    let enum_instance = AmazingEnum::V2 {
        something: another_value.unwrap(), // compiler doesn't complain about the value possibly being "None" while the enum expects the value to be of "i32" type
    };

    // but since the above compiles just fine, the following code panics during runtime:
    AmazingEnum::do_something(&enum_instance);
}

Obviously, we have "unwrap_or", "unwrap_or_default", etc.., to ensure that a value is not "None" when it needs to be "Some"thing, but since, for example, a match clause does not compile unless all possible variants of the enum are accounted for, wouldn't it also be suitable for a compiler to check possible erroneous variants of an Option enum passed to a field the type of which is concrete and known at compile time?

1 Like

Judging by these comments you are misinterpreting the behavior of your code.

Running it prints

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:23:34

where 23:34 is the location

        something: another_value.unwrap(),
                                 ^ here

The next line is not reached, the call to unwrap simply panics, as expected from Option::unwrap:

pub fn unwrap(self) -> T

Returns the contained Some value, consuming the self value.

Because this function may panic, its use is generally discouraged. Instead, prefer to use pattern matching and handle the None case explicitly, or call unwrap_or, unwrap_or_else, or unwrap_or_default.

Panics

Panics if the self value equals None.

Examples
let x = Some("air");
assert_eq!(x.unwrap(), "air");
let x: Option<&str> = None;
assert_eq!(x.unwrap(), "air"); // fails

All variants are accounted for.

In fact, if you look at how .unwrap() is implemented, you’ll see the None case is very clearly and explicitly handled. Is simply panics :slight_smile:

impl<T> Opiton<T>
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic("called `Option::unwrap()` on a `None` value"),
        }
    }
}

I’m not sure what you’re explaining here. In Rust, every type (including Option<T>) is concrete and known at compile time, so that’s nothing particularly special. A type like Option<i32> is not any way less concrete than i32, and in the eyes of the compiler Option<i32> has as much to do with i32 as with String. If you got the type wrong there’d be a type mismatch error.[1]

Also (and accordingly, as otherwise you’d get said error) there is no Option-typed value being passed into the (i32) field here at all. Option::unwrap called on Option<i32> returns an i32, then that is what you assign to the field :wink:


  1. fn main() {
        let another_value: Option<i32> = None;
    
        AmazingEnum::V2 {
            something: another_value,
        };
        AmazingEnum::V2 {
            something: String::new(),
        };
    }
    
    error[E0308]: mismatched types
      --> src/main.rs:21:20
       |
    21 |         something: another_value,
       |                    ^^^^^^^^^^^^^ expected `i32`, found `Option<i32>`
       |
       = note: expected type `i32`
                  found enum `Option<i32>`
    
    error[E0308]: mismatched types
      --> src/main.rs:24:20
       |
    24 |         something: String::new(),
       |                    ^^^^^^^^^^^^^ expected `i32`, found `String`
    
    ↩︎
6 Likes

Thanks for clarifying the details. I'm only starting my journey with Rust and might not use all the terminology correctly. But what I meant to convey in my post is that it would be nice to have a mechanism in Rust (that might already be in place, and of which I might just not know about yet) that would notify a programmer of a potential error in code.

I suppose Rust could be considered to follow the "If It Compiles, It Works" philosophy, but as in the case specified in my original post, even though the Option might be of "None" variant and therefore cause a panic during runtime, there is nothing that would notify a programmer about that error.

As you've said

But as we know that Option can also be of a "wrong" value (something that a compiler knows as well, since

). It doesn't appear like the compiler accounts for all the possible variants (doesn't account for when the value of an Option is None), because it compiles without any warning.

The filed of type i32 can only ever be of type i32, but the Option can have two variants that are not yet consider as different types, unfortunately.

And if compiler doesn't consider the possible "wrong" type of the Option enum, I thought maybe there is a "trick" of a technique to somehow make sure that the "something" field in my example only ever receives "successful" values of an Option enum. Like a marker on the type of the field itself:

something: Some(i32) // but this returns an error: expected type, found variant `Some` not a type

There's such a mechanism... that's precisely the defacto way in that the compiler works, but you explicitly told it not to notify you by explicitly calling unwrap.

4 Likes

The type of another_value.unwrap() is not Option<i32> but i32, which is the correct type for something, so there's no invalid value to consider. unwrap is just a regular method on Option that produces a value of the inner type. So in your code, something does not receive a value of the Option type (which would result in a compiler error), but i32. The way to ensure it only receives valid values of type i32 is to annotate it with the type i32, which you've done:

    V2 { something: i32 },

Yeah, I know

But if I have to, for example, pick a language with which not all of my team members are entirely familiar, I'd want that language and a plethora of tools that had grown around it to catch the potential errors a programmer might make, such as, in this case, not using a "safe" alternative of an "unwrap" method (which not an absurd thing to ask for, a simple check of a value of a monad, which the Option enum is).

I think I've already gotten an answer to my question with the pending RFC issue, which I have also linked above, which seems to be eagerly anticipated.

What you are asking is unreasonable, really. You could create custom lints to prevent calls to methods such as unwrap, but a compiler will never stop the human from writing certain code. Those methods are there precisely to empower the humans behind the keyboard, which are supposed to know better than the compiler.

1 Like

That's what clippy is for. You can ban the use of unwrap with https://rust-lang.github.io/rust-clippy/master/index.html#/unwrap_used, for example

The RFC you linked wouldn't be helpful here. You would still need to handle the None case somehow, and unwrap would still be a valid (and the quickest and easiest, thus a likely option for someone unfamiliar with the language to go for) way to handle it.

9 Likes

But in order for a human to use those empowering methods they would need to first consider using them, and how would they do that if nothing so far notifies them of a potential issue those methods where provided for in the first place (unless, as you've just mentioned, and as @ Heliozoa also pointed out below, these "notifications" — whatever form they may have — are customly set up. A human would surely know better than a compiler, but only after they encounter a runtime error, and humans are prone to making errors, so if someone is having a bad day, I hope you see how them forgetting to handle a possible None case is a probable situation to happen.

Oh, thanks, that actually seems like what I was looking for, it a little bit of a shame that some custom set up needs to be performed for these little issues to be caught, but I'm glad there is after all a way to "gracefully" introduce those automatic warnings. Thanks!

1 Like

In the language as it currently stands, Option::unwrap() is a way of choosing to handle a None case: "panic if None is found". If there were to be a warning by default on unwrap(), then there would have to be a new, different way of expressing "panic if None is found, yes, really, I mean it" (or normal, high-quality code in some cases would end up full of warning suppression, which isn't great). It's not possible for the compiler to distinguish “this unwrap() is going to be triggered and that's bad” from normal uses of unwrap() — it has neither enough context nor enough reasoning ability.

It has been frequently discussed that “unwrap” isn't a great name for this operation, but it's the name we've got, and changing it and all the documentation would be churn for not much gain. The programmer is expected to learn that unwrap() should be used thoughtfully.

9 Likes

Honestly, I sometimes feel like unwrap_unchecked should've been called unwrap, and unwrap should've not been a thing.

Edit:
I do want to note though, unwrap IS handling the None. You can not get into undefined behaviour land by using it, unlike something like casting reference to a pointer in C++.

1 Like

Relevant: Using unwrap() in Rust is Okay - Andrew Gallant's Blog

4 Likes

I don't follow you. One of the first things a beginner to Rust learns is that some things produce Option or Error instead of the value they want to use. They then immediately learn why Option or Error is returned and they have to handle it some how. Then they learn that unwrap() is a way to handle them but at the cost of the program terminating if they do.

It's a deliberate choice to handle these with unwrap as much as doing a match on them.

At least that was my experience working through the Rust Book which is what I assume everyone does.

I must admit that I am prone to use unwrap() a lot during initial development of a code idea. Deliberately, I'll think about handling errors when I have something working.

I would not want a clippy lint that complains about unwrap(), that would be annoying. I find a quick grep for unwrap() works well.

2 Likes

That is the way you do it. If you diligently go through all the unwrap statements in your code manually, one by one, reminding yourself of the behaviour a program should have for each case, good for you then.
But when working in big team on a giant codebase, I cannot be sure that everyone will be as diligent as you, I would want everyone to be on the same page, and an explicit warning from a compiler or from clippy definitely helps with that.

As to this point you're making:

See:

1 Like

But that's exactly the point of unwrap (which you are completely missinng). What you are asking for is redundant.

If you unwrap, you are guaranteeing that the Option is Some, otherwise the code will panic. If you don't want that, then don't use unwrap, and simply match on the option, in order to be forced to handle the None case in some way other than panicking.

1 Like

One available choice here is to avoid unwrap() and use strictly expect(); it does the same thing, panicking on None, but accepts a string to include in the panic message. That string can be used to document, justify, why the specific case should be expected not to be None.

If you want this additional rigor, then #[deny(clippy::unwrap_used)] is appropriate for your codebase. (The lint documentation even mentions this use case.)

8 Likes

I know what you mean about bad days. I have plenty of those. It's just that to my mind using unwrap() is not forgetting to handle a possible None case, it's deliberately handling the None case with unwrap() instead of a match. If programmers are having bad days they are likely to create all kind of logic errors that we cannot expect the compiler to know about. What if they use a match but don't do the right thing on None?

Then they are forced to code in Java for a week, one by one, entire team, with their families watching and all — a tragedy that can thankfully be avoided now that we've figured that there exist tools to help them do the right thing. How great is that, ay?

Ha, a cruel and unusual punishment :slight_smile: