rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
96.56k stars 12.47k forks source link

Tracking issue for Option::expect_none(msg) and unwrap_none() #62633

Closed cuviper closed 3 years ago

cuviper commented 5 years ago
impl<T: fmt::Debug> Option<T> {
    pub fn expect_none(self, msg: &str);
    pub fn unwrap_none(self);
}

Steps:

jfrimmel commented 4 years ago

Small and very minor documentation note:

If this has landed, we should adjust the #[must_use] attribute of Option::is_none() to keep symmetry with is_some().

Currently the hint on is_some() is "if you intended to assert that this has a value, consider .unwrap() instead", whereas the one for is_none() is longer and more verbose: _"if you intended to assert that this doesn't have a value, consider .and_then(|| panic!(msg)) instead". Once this methods are stable we should change the latter to "if you intended to assert that this doesn't have a value, consider .unwrap_none()"_.

cuviper commented 4 years ago

@jfrimmel yes, that's the third item in the checklist here.

jfrimmel commented 4 years ago

Oh, excuse me. Note to myself: read the issues completely before commenting. Sorry again.

cuviper commented 4 years ago

I guess it could use more in-repo callers? I do see that rustc_middle and rustc_mir are using expect_none, at least. Grepping for assert.*is_none shows a lot more possibilities, but maybe some of those would not like the added code to Debug the value in the panic message.

aclysma commented 4 years ago

I'd love to see this go to stable.. I'm having to do this:

let old_value = self.values.insert(key, value);
assert!(old_value.is_none());

And I'd much rather do

self.values.insert(key, value).unwrap_none();
dtolnay commented 4 years ago

In the stabilization PR it was raised that &self may be more appropriate and flexible in these signatures than self. Do potential users here have a preference for either?

RustyYato commented 4 years ago

You can use as_ref if you need a reference. Also, we should be consistent between our functions (Option::unwrap).

aclysma commented 3 years ago

I'd like to see unwrap_none be completely parallel to unwrap_err. Result has unwrap(self) and unwrap_err(self), so I think Some should likewise have unwrap(self) and unwrap_none(self).

naiveai commented 3 years ago

@aclysma what would unwrap_none return? Result::unwrap_err exists because the Err type still contains a value. But None doesn't.

aclysma commented 3 years ago

unwrap_none would return nothing if it is None and panic if it is Some. Here is a practical example of how I would like to use it:

EDIT: Oops! I didn't realize I already had this example above: https://github.com/rust-lang/rust/issues/62633#issuecomment-629670374 (More examples here: https://github.com/aclysma/rafx/search?q=old.is_none)

cuviper commented 3 years ago

It could be called assert_none, since there's nothing to return, but calling it unwrap_none is nice to mirror Result.

douglascaetano commented 3 years ago

My two cents on this topic after finding myself wanting to use these APIs:

Besides https://github.com/rust-lang/rust/pull/73077#issuecomment-643754745, I do believe there is value to add unwrap_none and expect_none methods. I do agree with the argument that we should avoid adding new methods to std, but these seem pretty natural to me.

As for the name of the methods, I believe unwrap_none is better than assert_none because it parallels with Result::unwrap_err and pairs with Option::unwrap. It is not an absolute incoherence to "unwrap" a None to a () (it seems quite logical, actually). Also, one already knows unwrap and expect methods, so it is a smaller cognitive burden to find (and memorize) all other similar methods, and we wouldn't open a can of worms by creating a new "group" of assert methods.

That said, I would either stick with unwrap_none and expect_none or remove these methods altogether.

John-Nagle commented 3 years ago

I'd argue for expect_none and unwrap_none as standard. Having them as optional "features" is a strange limbo state. That creates confusion as to whether they're staying around or going away.

Using "assert!" around an expression which must be executed for its side effects seems poor form.

My main use case for this is for collection "insert" operations where failure is not expected and should be fatal. It's tempting to write

let _ = somehash.insert(k,v);

but it's better to check. A concise syntax for checking is a good thing in this situation.

KodrAus commented 3 years ago

@rfcbot fcp close

We discussed this in the Libs meeting and decided against stabilizing these methods in favor of assert!(option.is_none()).

The reasoning was that all this method can do is either succeed with a () or panic, so is an assert, but that's already handled by the assert! macro.

rfcbot commented 3 years ago

Team member @KodrAus has proposed to close this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

rfcbot commented 3 years ago

:bell: This is now entering its final comment period, as per the review above. :bell:

aclysma commented 3 years ago

When I read the prior comments, I see more people in favor of merging than closing. In particular, I noticed a rebuttal to the rationale for this disposition that was thumbed up 10x (https://github.com/rust-lang/rust/issues/62633#issuecomment-751133919). I interpret this as strong consensus against the disposition.

Since we’re entering FCP, I want to briefly summarize why I would like to see this merged:

EDIT: My first link was not the comment I intended to link

trevyn commented 3 years ago

option.unwrap_none()

feels much lower cognitive complexity (two "chunks" in a linear reading style) than

assert!(option.is_none())

which is three chunks in a jumping-around reading style.

Especially important if it is a part of a larger statement.

buzmeg commented 3 years ago

If anything this is much closer to "Consensus Accept" and is certainly nowhere near "Consensus Reject". I see lots of people articulating in the positive direction and very few articulating negatives. Why is this being shut down?

I'd like to see some real reasoning for closing this given that it actually improves consistency with Option and Result, clearly had users even within the Rust codebase itself as well as in the "real world", and doesn't seem to have garnered any negatives.

In addition, I would certainly like to see someone address the "Using "assert!" around an expression which must be executed for its side effects seems poor form." comment which is a substantive negative comment about the proposed alternative.

I would also argue that the "chained" form of this expresses intent more properly than a random assert sprinkled in the next line. This is going to allow better optimization.

I can also tell you in the embedded space that it is not rare that we just blast assert!() from the codebase while leaving the more "intent" panics expressed like this alone. However, I generally try not to push "What gets done in embedded" too hard as we do very nasty things on a daily basis. :)

Thanks.

rfcbot commented 3 years ago

The final comment period, with a disposition to close, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

trevyn commented 3 years ago

Ok, how do we get a human review of this? :)

trevyn commented 3 years ago

I was a “weak merge” voice earlier, but after considering, I’m a “strong merge” voice. For one reason: It encourages a modern chained-monad functional programming style, which was also one of the considerations in the .await() postfix syntax decision, which I think has worked very well.

buzmeg commented 3 years ago

@KodrAus @Amanieu @BurntSushi @dtolnay @m-ou-se @sfackler

Why did this close given the comments flying around with no response?

m-ou-se commented 3 years ago

It didn't close yet. The bot just comments that 10 days after FCP started.


Marking this as waiting-on-team.

m-ou-se commented 3 years ago

Here's my view on this:

The main purpose of Option::unwrap() is to get the value held inside, which has no other option but to panic when the value isn't there. Panicking is not its main purpose. This is similar to indexing an array: a[3] will panic if the array doesn't have a fourth element, but that bounds check is not its main purpose.

For example:

let x = array[3]; // Panics if array.len() <= 3
let y = option.unwrap(); // Panics if the option is not a Some(..)

Here, if you were not interested in the value, but only in the panic, I'd expect that to be written as:

assert!(array.len() > 3);
assert!(option.is_some());

And not:

array[3];
option.unwrap();

Even though this code would also panic on the same conditions.

I agree that method chains can be a nice way to write things (and am a fan of postfix macros and keywords), but I don't think assert!(option.is_none()) should get a special method any more than assert!(v.is_empty()) or assert!(n < 0) or assert!(condition) should (e.g. v.unwrap_empty() or n.assert_negative() or condition.unwrap_true()).

(Maybe if we ever get postfix macros, we could have a option.is_none().assert!("oh no"). But that's a whole separate discussion.)

m-ou-se commented 3 years ago

Result::unwrap(); on the other hand does have added value over assert!(result.is_ok()): It puts the value held in the Err(..) in the panic message. That's something assert!() couldn't do by itself.

trevyn commented 3 years ago

I would expect that unwrapping is the main purpose of any unwrap function, and that the value held inside the wrapping is returned as a secondary consequence. Getting at this secondary consequence is often (but not always) the reason why we unwrap, but best not to conflate that with what unwrapping is.

Further, Option::None is not any ordinary enum discriminant, it explicitly represents a container — a wrapper — that contains no value:

Type Option represents an optional value: every Option is either Some and contains a value, or None, and does not. https://doc.rust-lang.org/std/option/index.html

It makes perfect sense to unwrap any container, even if that container contains no value. Reifying this into a return value can only yield () ("used when there is no other meaningful value that could be returned"), and is therefore known a priori, but this is a red herring.

Thus,

impl<T: fmt::Debug> Option<T> {
    pub fn expect_none(self, msg: &str) -> ();
    pub fn unwrap_none(self) -> ();
}

has a pure symmetry with Result:

impl<T: fmt::Debug, E> Result<T, E> {
    pub fn expect_err(self, msg: &str) -> E;
    pub fn unwrap_err(self) -> E;
}

Unlike Result::unwrap_err(), Option::unwrap_none() is not useful for its return value, but it is useful for different secondary consequences of the act of unwrapping — secondary consequences that we already use often withOption::unwrap() and Result::unwrap(). So we would expect by symmetry from Result that Option::unwrap_none() would be present and usable in the same way.

m-ou-se commented 3 years ago

I would expect that unwrapping is the main purpose of any unwrap function, and that the value held inside the wrapping is returned as a secondary consequence.

This confuses me. Removing the wrapper around a value is unwrapping, right? I'm not sure what you mean here with 'unwrapping' if it's not to get to the value.

Further, Option::None is not any ordinary enum discriminant, it explicitly represents a container — a wrapper — that contains no value:

Type Option represents an optional value: every Option is either Some and contains a value, or None, and does not. https://doc.rust-lang.org/std/option/index.html

The documentation explains that None has no value. It doesn't wrap anything, there's nothing inside of it. Only Some(..) wraps a value. (Unlike Result, where both Ok and Err wrap a value.)

It makes perfect sense to unwrap any container, even if that container contains no value.

But you can't unwrap something if there's nothing to unwrap. An Err(x) contains x (even if x is ()), but None doesn't contain anything.

any container

So do you think Vec should also have a vec.unwrap(), that would cause a panic for empty vectors?

Unlike Result::unwrap_err(), Option::unwrap_none() is not useful for its return value, but it is useful for different secondary consequences of the act of unwrapping.

Yes, that's exactly what I meant in my comment about using array[3]; only for its panicking side effect. It'd work, but isn't the purpose of that method/operator.

tanriol commented 3 years ago

One possible alternative that may be interesting for more than just Option would be to have assert_true and/or assert_false methods on bool as a general-purpose chainable assertion.

trevyn commented 3 years ago
method meaning
Result::unwrap() "Assume Ok variant and unwrap that, otherwise panic."
Result::unwrap_err() "Assume Err variant and unwrap that, otherwise panic."
Option::unwrap() "Assume Some variant and unwrap that, otherwise panic."
Option::unwrap_none() "Assume None variant and unwrap that, otherwise panic."

The panic when we Option::None.unwrap() does not happen because of any inability to "unwrap a None", but rather because the (implicit) assumption of a Some variant did not hold.

The documentation explains that None has no value. It doesn't wrap anything, there's nothing inside of it. Only Some(..) wraps a value. (Unlike Result, where both Ok and Err wrap a value.)

Option::None is singularly unique because it wraps (abstracts, contains, represents) the concept of there being no value and no type inside, which is a representable thing when unwrapped: ().

But you can't unwrap something if there's nothing to unwrap. An Err(x) contains x (even if x is ()), but None doesn't contain anything.

You can unwrap an empty 🎁.

So do you think Vec should also have a vec.unwrap(), that would cause a panic for empty vectors?

I think it would be coherent for Vec to unwrap to an owned slice, if such a thing were possible. Note that unwrapping an empty Vec in this way would not panic, has an obvious correct result, and represents something other than the complete absence of value and type.

impl<T> Vec<T> {
    pub fn unwrap(self) -> [T];
}

If the Vec's self did not exist, then it would panic, but of course there is no way to make that method call.

However, Option::<()>::None.unwrap() is callable, and indeed panics, because Option::unwrap() means "Assume Some variant and unwrap that, otherwise panic", as above.

Unwrapping None (rather than assuming Some and unwrapping that) is a symmetric and coherent idea — it succeeds and returns ().

Yes, that's exactly what I meant in my comment about using array[3]; only for its panicking side effect. It'd work, but isn't the purpose of that method/operator.

If any unwrap that doesn't use the resulting value is a misuse of the language, we've got a bigger problem.

Option::unwrap_none() is an ontologically coherent and useful method that means "perform the unwrapping of the expected None variant". It is an imperative statement that performs an action on a value, and the existence of this action is implied by symmetry from the existing actions on Result and Option.

In other words, I am interested in the action of unwrapping, not the panic, so this is not an assert, despite the isomorphism.

BurntSushi commented 3 years ago

I agree with closing this. Using a normal assert for cases like these is more than good enough. I also find the notion of unwrapping nothing quite strange and don't see a lot of value, if any, in adding it.

trevyn commented 3 years ago

I also find the notion of unwrapping nothing quite strange and don't see a lot of value, if any, in adding it.

It's no stranger than a function returning nothing.

There's prior art in dataflow programming: The production of the value (even though in this case it is the unit value) is the thing of interest, the purpose of the function. Same with unwrap_none(): The production of the value is the result of interest, not necessarily the value itself (or whether it exists). It is the reified essence of the method completing successfully.

The value behind the reified value of nothing is that we can think of more of the code as a pipeline, where the production and consumption of values is chained into meaning. An assert is a gate, an interruption in the flow. Postfix asserts would be an improvement, but they still come from the mindset of the error case, another code path to put on our mental stack. Option::unwrap_none() is an affirmation, a declaration that we are on the happy path, a statement that everything is flowing smoothly, that the container contained the expected contents, and any issues will be handled by the strict typing of the pipeline's components. (With our choice of error handling methodology being reflected by which components we choose.)

This is the same idea behind the ? operator — an intent to perform an unwrapping, a confirmation of happy path, a typed dataflow — with a preferred error handling strategy.

I'd love to be able do things like option.ok()?; as well for the same reason. Sometimes all you need is a line number to track down the problem. (See also: https://github.com/dtolnay/anyhow/issues/116)

BurntSushi commented 3 years ago

I understand what you're saying. I just don't find it compelling.

It's no stranger than a function returning nothing.

Well, I suppose we will have to agree to disagree. A function returning nothing is a routine and well understood thing. Unwrapping a "none" value is not.

aclysma commented 3 years ago

There are comments with lots of +1s from before and after FCP. Can we please get responses to those before closing this?

trevyn commented 3 years ago

@BurntSushi Do you agree that the existing methods on Result and Option suggest the possible presence of and specify the exact meaning of Option::unwrap_none(), and that the () which it would return meaningfully represents the result of an unwrapping?

BurntSushi commented 3 years ago

@trevyn No.

trevyn commented 3 years ago

Here's what's stable today:

Result<T, E> Option<T>
😃 unwrap() -> T unwrap() -> T
😞 unwrap_err() -> E

Every character of the fourth quadrant and its implementation is exactly specified by the rest of the chart, it performs a useful function, and it is missing from the language. I and multiple other people ended up here because we tried to use it. It is a phantom method that exists as a reflection of the other three, whether or not it is written down.

If it literally performed no function, was incoherent, sure, don't write it down. But it does do something, and useful at that, so if it's not written down, it's just .... missing.

BurntSushi commented 3 years ago

We're just going in circles. I already responded to that. A missing quadrant on its own is uncompelling to me.

cuviper commented 3 years ago

Is there any room in std for stuff that a fair amount of users want, but the libs-team would personally not use?

I think there's no real cost to adding these small methods, so the argument against seems mostly aesthetic to me.

naiveai commented 3 years ago

@cuviper The cost is inherent in increasing the API surface. No code is better than no code, as they say.

BurntSushi commented 3 years ago

I haven't been arguing from the stance of whether I would use this or not. I've been arguing from the stance of whether it's a good fit for std.

There is definitely a lower cost to adding methods like these on to existing types when compared to other types of API additions. And we have historically had a lower bar for such methods. But the cost isn't zero. Unwrapping an absent value doesn't cut it for me.

Changing our policy to rubber stamp APIs based on some upvotes doesn't sound like a good idea to me. Utility to folks is and should be a factor in our decision. Folks have made their case. I personally remain unswayed.

trevyn commented 3 years ago

@BurntSushi Based on your "No." response above, I'm concerned that I may have failed to even express my argument in a way that is understandable.

Do you agree that the existing methods on Result and Option suggest the possible presence of and specify the exact meaning of Option::unwrap_none(), and that the () which it would return meaningfully represents the result of an unwrapping?

I believe these are factual statements, above opinion and potential for disagreement. Could you be specific about where you disagree?

cuviper commented 3 years ago

It may also be worth reiterating the original motivation from #62431 -- Option::is_none has a rather verbose must_use suggestion because there's no simple method to make that assertion.

#[must_use = "if you intended to assert that this doesn't have a value, consider \
              `.and_then(|| panic!(\"`Option` had a value when expected `None`\"))` instead"]
pub const fn is_none(&self) -> bool

I think using and_then when you expect None is really beating around the bush. If we (collectively) really think that it's best to wrap this as assert!((expr).is_none()), then must_use should say that.

BurntSushi commented 3 years ago

I believe these are factual statements, above opinion and potential for disagreement. Could you be specific about where you disagree?

If you intend for them to be facts, then I probably agree with them. Otherwise, my disagreement lay in the fact that I think unwrap_none doesn't make a lot of sense. And the presence of certain methods on Result do not necessarily specify the behavior of unwrap_none. For example, I think it's just as reasonable to assume that since "none" represents absence, that there therefore is no analog to unwrap_err for Option. That is, the quadrant missing in your table is totally reasonable and not unexpected from my perspective.

naiveai commented 3 years ago

I'm really not super sure what the value is in arguing with core maintainers by claiming your statements are above disagreement over something that maybe had a decent amount of a support at one point but now no longer does.

trevyn commented 3 years ago

@naiveai My intent is to improve the Rust language by identifying differences in mental models of how the language is structured that can be resolved through discussion. That Result and Option exist in the language is beyond disagreement — it is a foundation on which we can build our models and our interactions. I'm trying to understand where our shared foundation actually is.

@BurntSushi Thank you. It sounds like you believe that a None value should not be able to be "unwrapped", is that correct?

Specifically, I can't find a place in the Rust documentation that defines what "unwrap" means in an abstract sense, and surprisingly, I also could not immediately find a reputable direct definition of "unwrap" in a computer science sense that applies to the Rust usage of the term. I think it would be appropriate and generally helpful to clearly define the term, as Rust uses it.

buzmeg commented 3 years ago

I don't really think that support actually went away. People with real uses cases are still here.

And the language developers have a very different view from the end users.

I am in the camp of thinking about the "unwrap machinery" rather than the value. From the developers view, there is no difference.

However, if that is really true, should that not be locked down? Or is this semantic being "reserved" for some reason? Is there an implementation complication?

While there is value in "minimal API" there is also value in "specified API"--especially when the API to be added looks like a hole rather than purely a new feature. This feature is useful enough that when the language has the ability to support it--even if horribly badly--it will reappear, it will gain some level of popularity, and the developers will not have control of the semantics.

As for embedded, I can almost certainly cough up a chain of operations that can't be broken apart into that separate assert but I probably can't limit it unwrap_none()--something else will bite first. Memory that can read but not write, write but not read, partial ownership of a byte, etc.--a lot of things get expressed in weird ways because we have nasty edge cases and can't always be converted to "obviously equivalent" alternatives. However, if you're not convinced by earlier examples, you'll just call mine tortured (and, to be fair, that's likely true).

At the very least, the folks developing this have broken radio silence, so we now have some rationale. However, please do expect some pushback when the rationale is "We don't think this is useful enough" (subjective aesthetic objection) as opposed to "This has unclear, difficult to define semantics." (objective technical objection)

Thanks.

trevyn commented 3 years ago

Oh my, it's worse than I thought: #68918 Don't use the word "unwrap" to describe "unwrap" methods

SOF3 commented 3 years ago

It appears that the controversy is mainly about unwrap_none. Could we stabilize just Option::expect_none first?

aclysma commented 3 years ago

@SOF3 just curious, do you feel that unwrap_none is harmful or is it just that expect_none is better than nothing?

I do think expect_none is much better than nothing. (I can use expect_none("") to achieve unwrap_none(), it's just less "nice")

That said:

I realize this isn't a pure-numbers "vote" but I would like to know what is so disliked about these tiny methods that makes it worth going against the clear consensus.