Closed cuviper closed 3 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()
"_.
@jfrimmel yes, that's the third item in the checklist here.
Oh, excuse me. Note to myself: read the issues completely before commenting. Sorry again.
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.
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();
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?
You can use as_ref
if you need a reference. Also, we should be consistent between our functions (Option::unwrap
).
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).
@aclysma what would unwrap_none
return? Result::unwrap_err
exists because the Err
type still contains a value. But None doesn't.
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)
It could be called assert_none
, since there's nothing to return, but calling it unwrap_none
is nice to mirror Result
.
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.
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.
@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.
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.
:bell: This is now entering its final comment period, as per the review above. :bell:
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
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.
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.
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.
Ok, how do we get a human review of this? :)
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.
@KodrAus @Amanieu @BurntSushi @dtolnay @m-ou-se @sfackler
Why did this close given the comments flying around with no response?
It didn't close yet. The bot just comments that 10 days after FCP started.
Marking this as waiting-on-team.
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.)
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.
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: everyOption
is eitherSome
and contains a value, orNone
, 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.
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: everyOption
is eitherSome
and contains a value, orNone
, 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.
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.
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. OnlySome(..)
wraps a value. (UnlikeResult
, where bothOk
andErr
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)
containsx
(even ifx
is()
), butNone
doesn't contain anything.
You can unwrap an empty 🎁.
So do you think
Vec
should also have avec.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.
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.
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)
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.
There are comments with lots of +1s from before and after FCP. Can we please get responses to those before closing this?
@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?
@trevyn No.
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.
We're just going in circles. I already responded to that. A missing quadrant on its own is uncompelling to me.
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.
@cuviper The cost is inherent in increasing the API surface. No code is better than no code, as they say.
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.
@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?
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.
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.
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.
@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.
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.
Oh my, it's worse than I thought: #68918 Don't use the word "unwrap" to describe "unwrap" methods
It appears that the controversy is mainly about unwrap_none
. Could we stabilize just Option::expect_none
first?
@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.
Steps:
must_use
message onis_none
(per https://github.com/rust-lang/rust/pull/62431#pullrequestreview-258576936)