Closed m-ou-se closed 1 year ago
During the discussion in the libs-api meeting just now, there was not only some discussion about whether to take self by reference or value (which didn't reach a definitive conclusion), but also some on whether this method should exist at all.
@dtolnay brought up how he preferred for is_some_and
not to be a specific method for Option
, but instead for that "is some and" operation to written as three separate words using a new language feature, like is
Some
&&
(e.g. opt is Some(x) && x > 5
).
This specific syntax was just an example, but the point we generally seemed to agree on was that it's a shame that our Option
and Result
type have so many methods, even though they're supposed to be these really simple types. Ideally, we'd have some kind of language feature making methods like is_some
, map_err
(etc. etc.) no longer necessary for ergonomics. Then questions like "should Poll::is_ready() exist?" are no longer relevant, and everyone's custom enums all become nicer to use.
The part that there were some different opinions on was if, for now, we should keep adding things like is_some_and
, before such language feature happens (if it ever happens), or if adding methods like that makes things worse.
So, regardless for whether this feature goes through or not, I'd like to encourage ideas and exploration of a potential ergonomic language feature that would cover a large chunk of all Option and Result methods. (Not on this thread though! https://internals.rust-lang.org/ and Zulip are better places for that.)
@m-ou-se I'm currently putting together a comprehensive list of which Option
and Result
methods would make sense to replace with such syntax. (There are still plenty that don't, but many do.)
This reminds me of the let-expression RFC https://github.com/rust-lang/rfcs/pull/3159. It would allow to write this
assert!((let Some(x) = a) && (let Some(y) = b(x)) && x == y);
I don’t quite fully understand what the benefit of this method over using matches
is, and I don’t see it addressed anywhere in this discussion. I understand that something like foo.is_some()
is more neat than matches!(foo, Some(_))
, but once a closure gets involved and the name becomes long and non-intuitive like in this proposed method, I don’t see the benefit anymore.
One possible benefit is that it’s a postfix / method-style expression. But if we ever get the ability to do postfix / method-style macros, then optional_deadline.matches(Some(d) if d < now)
is probably one of the first adaptors.
I could find this remark in the linked other issue
// Different ways to check if the `deadline` has passed (if there's a deadline), all a bit verbose or confusing:
let a = deadline.map(|d| d > now).unwrap_or(false);
let b = deadline.map_or(false, |d| d > now);
let c = matches!(deadline, Some(d) if d > now);
let d = deadline.map(|d| d > now) == Some(true);
let e = deadline.filter(|d| d > now).is_some();
let f = deadline.iter().any(|d| d > now);
Many of these are “confusing” because they use or combine functions beyond their main intended use case, or treat bool
s as values (rather than conditions) too much. In particular
a
and b
use API that’s almost as general as a full match
d
and e
involve a Option<bool>
value, which is confusing because we aren’t used to ternary logicf
uses an iterator, and any
’s naming makes more sense with iterators that can contain multiple itemsHowever c
uses matches
exactly as it’s intended to be used. This is no weird case, corner case, not even a slight abuse of a tool that’s generally applied to do other kinds of things like .any
typically handling iterators with multiple values, nor does it combine multiple things together in unexpected ways, in fact it’s a single-step process, a single call to matches
with a straightforward pattern and a straightforward guard.
So all I can take away from that post is that, apparently / maybe, matches!
is considered “a bit verbose”, since I can’t find any way to claim it “confusing”.
Comparing
matches!(deadline, Some(d) if d > now)
vs.
deadline.is_some_with(|d| d < now)
the former is … well … syntactically longer, though that’s mainly due to the words matches
+if
being longer than is
+with
; and, as mentioned above, matches!
is not postfix (yet). So maybe… let’s just work on that (postfix macros)?
is_some_and
can be written with matches!
, that is true... the dual is_none_or
cannot, though, I think?
is_some_and
can be written withmatches!
, that is true... the dualis_none_or
cannot, though, I think?
I guess is_none_or
is not straightforward with matches!
, indeed. However, that would probably not be an argument in favor of introducing only is_some_and
and not is_none_or
as is the proposed first step here, as far as I understand. (In fact, following this argument, is_none_or
should probably come first, have some merit (being a commonly useful method, etc…) on its own, and only then would a separate discussion about including is_some_and
for consistency make sense.)
If we had … and I don’t know if that’s a good idea, and also that’s probably fairly off-topic in this thread … something like a general way to make a guard part of a pattern, then maybe deadline.is_none_or(|d| d < now)
could be written as something like deadline.matches!(None | Some(d if d < now))
or deadline.matches!(None | (Some(d) if d < now))
. Which reads similar “deadline matches None
or …” with the “or”-pattern.
Hi - not sure what the best way to show support for this is, but weighing in to say that I'd very much appreciate this (or some variant of it, like the language extension proposed above.)
I am working with an ECS that lets me retrieve components through an Option
; I find myself needing to evaluate predicates against those components reasonably often (e.g. get(tags).map_or(false, |t| t.contains(a_tag)))
).
Unfortunately, the ways of currently doing this aren't easy to parse for a reader - map_or(false, predicate)
or map(predicate).unwrap_or_default()
both obscure the primary objective of the operation to me (it's not really clear what either of those are doing unless you've seen them before)
I would much rather be able to do get(tags).is_some_and(|t| t.contains(a_tag))
(or some variant thereof), which I find to be vastly more readable / semantically obvious. (In a sense, this is me responding directly to BurntSushi's comment from above; I think the proposed alternative is much clearer than what currently exists!)
That being said, if it is possible to replace the majority of the Option
/Result
combinators with something more general-purpose within the language itself, I would also love that. I'm really just throwing my weight behind the general principle 🙏
@camsteffen Apologies for the extra work, but would you be willing to make a draft PR that 1) modifies the signature of
is_some_and
(I saw that you had a PR for that previously), and then 2) includes an updated version of #98427 that shows what that would look like with a by-value signature?
Here you go! #99087
Thank you, @camsteffen!
Looks to me like by-value improved the vast majority of cases.
Suddenly it feels like having map_or is enough, it seems unnecessary to make Option Result more bloated if it's just for readability without any other advantages. (please feel free to ignore my statement
Looks to me like by-value improved the vast majority of cases.
If we do decide to go with that then the docs should have an example using .as_ref()
.
I'd like is_none_or
. I have a code block like this:
list.iter().filter(|x| {
if let Some(symbols) = &symbols {
symbols.contains(&x)
} else {
true
}
})
it could instead be:
list.iter().filter(|x| {
symbols.is_none_or(|symbols| symbols.contains(&x))
})
Alternatively right now I could write
symbols.map(|symbols| symbols.contains(&x)).or(true)
but it doesn't really read well imo.
@rfcbot resolved ref @rfcbot concern should-take-self-by-value
(This is slightly tangential but also a potential argument against adding a new method when there's an existing, almost as concise alternative:)
Hmm. To me the obvious way to write "is_some_and()" has always been filter().is_some()
rather than map_or
:
optional_deadline.is_some_and(|d| d < now);
vs
optional_deadline.filter(|d| d < now).is_some();
but I haven't seen it mentioned in this (or the previous) thread. Maybe it's less idiomatic than I thought? I noticed that Option::filter
has only been stable since 1.27 so there's maybe that?
I didn't even know filter
existed, I would have thought you would have to into_iter
first. Nice!
RFC: map_or_default in Option and Result
We should add a single map_or_default
rather than is_some_and
/ is_ok_and
.
I don't think map_or_default
is the right solution here: if anything, it's less readable than a plain map_or
.
I don't think
map_or_default
is the right solution here: if anything, it's less readable than a plainmap_or
.
Agreed, just to be another data point: I virtually never use the *_or_default
methods for primitives. Using unwrap_or*
as an example, not only is .unwrap_or(false)
shorter than .unwrap_or_default()
, but it is one less layer of cognitive indirection (in terms of what an internal monologue might looks like: "unwrap or default. what is the default? false". potentially worse if it takes a second to think about what type).
Maybe map_or_default
is not the right solution here, but it's better than is_some_and
/ is_ok_and
/ is_err_and
.
Adding my own data point in support of is_none_or
. My current use case is parsing a language with significant whitespace.
let foo = tokens.last().map(Token::kind);
let n = Token::Newline;
// `bar` should be true iff the latest token wasn't a newline or if this is the beginning of the source code.
let bar = match foo {
Some(x) if x == n => false,
_ => true,
};
let bar = foo.map_or(true, |x| x != n);
let bar = foo.is_none() || foo.unwrap() != n;
let bar = foo.is_none() || foo.is_some_and(|x| x != n);
let bar = foo.filter(|y| y == n).is_none();
let bar = foo.is_none_or(|x| x != n);
I hope you agree that using is_none_or
provides the shortest and most readable solution.
I'd be happy to approve a PR that adds is_none_or
as an unstable function. :)
I would have a use case for this too.
I have a VecDeque
and at some point I want to find out if I need to pop the front.
I would do this if:
if self.entries.get(1).is_some_and(|second| second.value == value) {
self.entries.pop_front();
}
Hexerator is currently using this https://github.com/crumblingstatue/hexerator/commit/8d643c5153dbe366b55b607972031cabaa7705c2
Since it is unclear to me that the past few examples are aware of matches
(I certain wasn't!), here's some rehashing:
matches!(self.entries.get(1), Some(second) if second.value == value)
matches!(get(tags), Some(t) if t.contains(a_tag))
Hey! Just passing by to say that the feature gate was renamed to is_some_and
, but the tracking issue wasn't updated, still says is_some_with
.
I have a use case for it too:
fn is_draw_full_cell_enabled(color_cell: Option<Rc<RefCell<ColorCell>>>) -> bool {
if let Some(color_cell) = color_cell {
color_cell.borrow().draw_full_cell
} else {
false
}
}
match event {
Event::MouseButtonPressed { button, x, y }
if is_draw_full_cell_enabled(
color_grid.mouse_position_to_cell_mut(Vector2::new(x, y)),
) => {}
_ => {}
}
Could just be:
match event {
Event::MouseButtonPressed { button, x, y }
if color_grid
.mouse_position_to_cell_mut(Vector2::new(x, y))
.is_some_and(|cell| cell.borrow().draw_full_cell) => {}
_ => {}
}
My code would be far simpler if I could just do that.
Looks like this got stuck at some point. I believe the main thing left to do was to change it to take self
by value, which already happened in #98354.
Let's start a new FCP.
@rfcbot cancel
@rfcbot merge
@m-ou-se proposal cancelled.
Team member @m-ou-se has proposed to merge 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.
Was there a resolution to the concern that the same behavior could be achieved with matches
?
@SUPERCILEX is_some_and
looks clearer and chains more easily. matches!
requires going back to the beginning of the expression to open a matches!(
, and looks more complex when embedding an if
in it.
:bell: This is now entering its final comment period, as per the review above. :bell:
@joshtriplett Fair enough, thanks!
The final comment period, with a disposition to merge, 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.
This will be merged soon.
Is this just waiting for somebody to create a stabilization PR? I can probably do that if so.
Is this just waiting for somebody to create a stabilization PR? I can probably do that if so.
Yes. Feel free to go ahead.
The description of this API in this tracking issue and in #110019 say that the closure takes &T
but the implementation has the closure taking T
.
We had a second FCP to change the closure to take T
, but it seems that we forgot to update the issue description. I've just updated it.
I totally forgot about all that.. Well I guess I will live with it. The draft PR to show that it made is_some_and
calls "nicer" unfortunately didn't show any places where taking ownership of of the inner value was necessary, but maybe there are a few and it would probably be pretty annoying for some people for the combinator to not be applicable therethr same way I find the extra as_ref
annoying where it is needed..
Well, I don't know if these functions are really that essential. These checks can be made in several different ways.
let data = Some("foo");
if data == Some("foo") {
//...
}
if data.is_some_and(|inner| inner == "foo") {
//...
}
if let Some(inner) = data && inner == "foo" {
//...
}
if matches!(data, Some(inner) if inner == "foo") {
//...
}
The first might only be desirable in some cases, but the third one with let-chains
https://github.com/rust-lang/rust/issues/53667 is a little bit more legible, in my opinion.
In which version should we expect this feature? I thought it was going to be in 1.70.0, but apparently not.
It is in 1.70.0 (see https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html)
Feature gate:
#![feature(is_some_and)]
This is a tracking issue for
Option::is_some_and
,Result::is_ok_and
andResult::is_err_and
.Public API
Steps / History
self
by value: https://github.com/rust-lang/rust/pull/98354Unresolved Questions
is_some_with
/is_ok_with
/is_err_with
is_some_and
/is_ok_and
/is_err_and
contains
/contains_err
and_is
/and_err_is
is
has
self
or&self
?