Closed m-ou-se closed 1 year ago
What are the use-cases? And won't that largely be covered by if-let chains?
if let Some(foo) = opt && foo > bar {
}
Lots of functions on Option
and Result
are already 'covered' by match
or if let
, but it's still very useful to have methods for the most common cases. It makes things more readable and less verbose, and also easier to write (especially with auto completion).
let deadline_passed = optional_deadline.is_some_with(|d| d < now);
let deadline_passed = matches!(optional_deadline, Some(d) if d < now);
let deadline_passed = if let Some(d) = optional_deadline && d < now { true } else { false };
I assume deadline_passed
would then flow into another if
in which case one could do an if-let-chain (once that's stable) directly rather than having a separate variable for that. I mean as I understand it the purpose of if-let-chain is to make such towers of conditionals more readable.
Maybe it's useful inside a filter
closure? But then one could flatten()
first and then filter
instead.
I think a code example with a bit more context that would benefit from this and wouldn't be replaced by if-let-chain in the future would be helpful.
Naming. Alternatives:
Or maybe just is
? Assuming we want to spend our small budget of meaningful two-letter words on this.
But I like is_some_and
since it is consistent with the way the if is written.
is
or any name ending in "is" will not flow well with function names in the argument: opt.and_is(char::is_lowercase)
, dog.and_is(Dog::has_a_bone)
, etc.
is_some_and
flows better. I would consider the shorter some_and
as well.
I assume
deadline_passed
would then flow into anotherif
in which case one could do an if-let-chain (once that's stable) directly rather than having a separate variable for that.
This example about a deadline: Option<Instant>
comes directly from code I worked on recently. Checking some condition on a Some
is a pattern that occurs reguarly there. In some cases it's just used in an if
directly, but in many cases the value is stored in some struct first, or used by multiple if
s, or passed to a function, serialized and sent over a socket, etc. In some cases it's only used in a single if
, but part of a much more complicated condition. Various parts of that condition are first assigned to named bool
s to keep things clear. (Just like why people use matches!()
rather than a match
or if let
in some situations. Or x.is_some()
rather than if let Some(_) = x
. Or x.then()
instead of if x {}
. And so on.)
Or maybe just
is
?
I've added is
and has
to the alternatives.
There are 117 cases of .map_or(false,
in the compiler/
directory in the rust repository. Most of them are probably good examples of use cases for these methods.
The name is_some_and
would allow us to add its counterpart is_none_or
in the future.
is_some_and
After thinking about it a bit more, I like this name. It very clearly states that it first checks is_some
, and
then also checks a condition afterwards.
FWIW this was attempted in #75298.
As someone who finds map_or both overly verbose for this (I need this version far more than the general case of map_or) and generally unpleasant (due to argument order not matching the name, although I 100% understand that decision even if I don't love it) I would be thrilled for this.
I think the worry about overlap with if/let is valid but for filtering and other combinators this composes really well (having played with it some now)
Also +1 to is_some_and
for the name, very much a fan
I often define similar functions in my code as an extension trait, and is_some_or
is exactly the name i used (before it was added to nightly rust). I think it's a very intuitive name.
I would like to also see the is_none_or
counterpart added to Rust. Note that is_none_or
cannot be easily expressed using if-let chains.
Would love to see this stabilized. My usecase is checking if a submitted value matches the last value, if there was a last value:
//Earlier, for example:
let last : Option<u64>;
let next : u64;
//In logic
if last.is_some_and(|v| v == next) {
return Err(());
}
@AmitPr if last == Some(next)
should also work, right?
@m-ou-se: not if next
is expensive.
It seems like everybody is happy with the current names for these methods. I think it's time to consider stabilization.
@rfcbot merge
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.
what is with is_none_or
?
Btw, I found this issue here by explicitly searching for is_none_or
approximately 30min after I discovered is_some_and
just because the later implicated the former's existence.
is_none_or
is a separate method that has been suggested but isn't part of this tracking issue.
Thx, does this mean we have to create a new issue for that method or is it somehow possible to attach it to this issue? I can also do the impl, just not sure how the process is.
I think the function should take the self
instead of &self
to be consistent with other function-taking methods like map_or
. That makes this function strictly more useful since the predicate can take ownership of the inner value.
It might be safe to say that predicate functions usually don't need ownership, but the API shouldn't impose that. There will be some cases where this is required or more efficient. You can always use as_ref().is_some_and(..)
to avoid consuming the Option
if needed. is_some
doesn't take self
because there would be no benefit in doing so, and it would force users to write as_ref().is_some()
in some cases.
I think functions named is_
shouldn't consume self
. If you want to consume it, I feel like a map_or
would be more appropriate. Making many use cases of is_some_and
more verbose by forcing them to use .as_ref()
doesn't seem great.
Thx, does this mean we have to create a new issue for that method or is it somehow possible to attach it to this issue? I can also do the impl, just not sure how the process is.
You can propose it here: https://github.com/rust-lang/libs-team/issues/new?assignees=&labels=api-change-proposal%2C+T-libs-api&template=api-change-proposal.md&title=%28My+API+Change+Proposal%29
I think functions named
is_
shouldn't consumeself
.
I see where you're coming from and I can get behind that. My only reservation is that I think this method will most often be used on a temporary value, like some_function().is_some_and(...)
and in that case it feels weird to be given a reference when is_some_and
is the sole user of the Option
before it gets dropped. This is even more likely the case for Result
. On the other hand I can see how, in other cases, is_some_and
causing a "moved here" error could be surprising.
I just opened a PR using the method in rustc. I needed to destructure the reference (as in is_some_and(|&foo| ..)
) a lot to get around type errors. Not really a big deal, the "extra reference" scenario is pretty common in Rust. There were only two instances where I had to stick with map_or
because the value was not Copy. But I think rustc deals with a disproportionate amount of Copy values and other code repos will hit that scenario (must use map_or
) more often.
As another data point, if my rustc PR is changed with is_some_and
taking by value, as_ref
is needed 4 times.
I like is_some_and
as replacement for map_or(false, ...)
. But then the obvious question is, do we also want a similar function for map_or(true, ...)
? The natural name would probably be something like is_none_or
(or is_none_or_else
, since the closure-taking functions usually are called or_else
, but that is more verbose than I like -- and we are not using is_some_and_then
, either).
rg 'map_or\(true' compiler/ | wc -l
says there are at least 37 uses of map_or(true, ...)
in the compiler sources.
EDIT: Ah, looks like that has already been proposed but just not implemented yet. :)
Regarding the signature, since I view this primarily as an easier-to-read replacement for map_or(false, ...)
my vote would be for consuming self
. If someone wants the by-ref version they can use .as_ref().is_some_and
; there is no equally readable replacement the other way around.
I think functions named
is_
shouldn't consumeself
. If you want to consume it, I feel like amap_or
would be more appropriate.
I find map_or(true/false
rather error-prone; each time I see them I have to stop and think carefully about what happens. is_some_and
/is_none_or
make such code a lot more readable. Hence I think we should have a by-value function with those semantics.
If the leading is
is the problem, we could use some_and
/none_or
, i.e., drop the leading is_
?
Making many use cases of
is_some_and
more verbose by forcing them to use.as_ref()
doesn't seem great.
At least those uses have the option of using as_ref
; no such option exists with the current API for when you need by-value semantics (other than falling back to map_or
, which I find sufficiently hard to read that I usually prefer a match
).
Also, how often do you really need the by-ref version? At least in rustc itself, this seems to be rare based on what @camsteffen said.
Seems like the feature gate name needs to be updated as well (is_some_with
→ is_some_and
).
I think this method will most often be used on a temporary value, like some_function().is_some_and(...)
I needed to destructure the reference (as in is_some_and(|&foo| ..)) a lot [..]
Oh, interesting. Every single time I wanted this function, it was on something that I did not want to consume. I didn't realize consuming would be such a common use case.
Aborting the FCP, because we clearly need to figure this out before it's ready for stabilization. :)
@rfcbot cancel
@m-ou-se proposal cancelled.
It does seem like most combinators for option and result types use values. Having to call as_ref
doesn't seem too onerous, and doing so won't be necessary for copy types.
Having is_some_and
consume the option would conflict with Rust's (and Option
's) existing "policy" of having is_
functions take &self
, so IMO a different name would be needed in that case.
The same could be said about a "policy" of is_
methods not taking any arguments, I'm not sure either is a useful metric to apply here.
True, although there appear to be some nightly methods starting with is_
that take arguments: https://doc.rust-lang.org/nightly/std/primitive.slice.html#method.is_sorted_by
And IMO is_
taking arguments is a less surprising extension than it taking ownership.
Is there any reason to actually consume Option
here?
IMHO in my use cases most of the time I only need to check value inside Option
and consuming it will be to a detriment. Basically for consuming we already can use and_then
.
PS: same arguments go for Result
.
I almost always want to consume the value, since it is a temporary that will anyway not be used afterwards. Having to use map_or
or and_then
makes the code a lot harder to read.
If you don't want to consume it, you can use as_ref
, like with any other Option
function, and then is_some_and
(x.as_ref().is_some_and(...)
). Basically all of them consume their argument, if there is any reason for them to. is_some
/is_none
are exceptions because it would be entirely pointless for them to consume their argument.
is_some
/is_none
are exceptions
@RalfJung, I don't know about you, but for me is_some_and
is very familiar to is_some
, it just takes function to customise the result. It becomes very confusing with such names, as most other places (maybe there are exceptions) don't change received type when adding suffix for the method name.
This just doesn't seem intuitive in a slightest, at least for me.
As I stated above, for me is_some_and(f)
is syntactic sugar for map_or(false, f)
. This improves readability by a lot; I find the map_or
invocation so hard to read that I often rather write a match
despite its verbosity. (One part of this is probably the occurrence of "or" in the name, even though this is logically an "and" as indicated by is_some_and
. Having both map_or
and and_then
is quite confusing naming.)
@RalfJung, I'm only agains the naming, not your idea in general. If is_some
takes reference, is_some_xxx
should take a reference as well. I'd prefer a different naming for consumption that doesn't have such conflicts.
You're talking about confusing naming, but nevertheless have no problems with adding even more "types" of consumption prefixes for Option methods that behave differently from what we already have.
Also your approach will forbid me from calling is_some_and
for option arguments that are already a reference and cannot be consumed, which I think is much worse than not liking map_or
syntax.
Also your approach will forbid me from calling is_some_and for option arguments that are already a reference and cannot be consumed, which I think is much worse than not liking map_or syntax.
This has been discussed above; you can just use x.as_ref().is_some_and
. That still has the clear readability of is_some_and
.
Making it consume ownership is strictly more general/powerful than making it take a reference.
I am not tied to the name either, and also stated that above already. :)
If the leading is is the problem, we could use some_and/noneor, i.e., drop the leading is?
Since the non-consuming version leaves the option unusable afterward, maybe it should be called was_some_and
(half-joking here, but if it had any precedent it wouldn't even be a bad name IMO).
I doubt the mismatch in the signature with is_some
would actually cause much or any confusion in practice. When using is_some
, I don't give a thought to what the signature is. I agree there's value in consistency, but I think that's a small loss when weighed against the benefit of being a total replacement for map_or(false,
. Also consider &self
is useless for a Option<&mut T>
.
Sure we could resolve the tension by using a different name, but I feel that is_some_and
is a very excellent name that is unrivaled for readability. It's extremely clear and even has smooth grammatical flow in real usages.
In my mind, and_if
is the one other viable name option. That name seems to have been forgotten since the other thread. It's more analogous to and_then
, which takes self
.
It's not clear to me that something like is_some_and
is better than map_or(false
. That is, given the choice, I'm pretty sure I'd take the latter option almost every time. I think that the former could be justified if it became a core part of someone's vocabulary, but I don't think it will get used enough for that. map_or
on the other hand is virtually self-describing, and makes the fallback value (when the Some
isn't present) very clear. But is_some_and
kind of requires thinking about it a little bit and realizing that and
implies that false
is returned if it's a None
value.
Here's my main point: if I stumbled across these routines in a code review without previous knowledge of them, I'd probably have to look it up. If I stumbled across map_or
without previously knowing about them, I probably wouldn't have to look it up. Obviously, this isn't a universal litmus test (I might have to look up and_then
too, for example), but it's what stands out to me the most for this case.
To be clear, I don't think I feel super strongly about this. But as it stands, I'm not totally convinced these are worth adding.
As a small data point, I grepped through the projects I'm mostly working on (Ruma and matrix-rust-sdk) and found that out over more than a dozen, only two or three cases of .map_or(false, ...)
were preceded by .as_ref()
. I'm also pretty sure none of the .map_or(false, ...)
invocations needed to consume the Option
though.
This matches my intuition / preference towards the current signature; I think the readability of is_some_and
isn't the only advantage over map_or(false, ...)
, not having to call .as_ref()
first to avoid consuming the Option
is also an advantage IMHO.
In my mind,
and_if
is the one other viable name option.
I think that's substantially less readable. is_some_and
felt intuitively obvious the moment I saw it. and_if
reads nicely in an if
, but it doesn't read as nicely in any other context.
opt.is_some_and(f)
is also equivalent to opt.into_iter().any(f)
or opt.iter().any(f)
. So a possible name would also be just any
(and all
for is_none_or
).
Iterator::any
also already takes a value, so no new precedence and it would be consistent naming.
Scala does this with its Option.exists
/ Iterable.exists
.
I really don't think we should bikeshed the name further; I think we have enough bikeshedding to do regarding ref vs non-ref argument.
I really like the argument that is_
implies &self
, whereas some of the other names (and_if
, etc) imply self
.
So I think the two questions are tied together.
@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 https://github.com/rust-lang/rust/pull/98427 that shows what that would look like with a by-value signature?
That would give us a substantial sample of whether in practice the by-value version would work better or worse for a large codebase. If we end up with a lot of calls to .as_ref().is_some_and(...)
, that suggests we might want the by-ref version. If most of the code is either the same or can drop a &
or *
, that suggests that we want the by-value version.
Meanwhile, let's gauge consensus on adding this method orthogonally to whether it takes self
or &self
.
I'm going to FCP this and immediately raise a blocking concern. I intend to leave the concern until we see how the by-value version works out in practice (ideally via a draft of the aforementioned PR).
If you would approve this either way, or if you would approve whichever version makes for simpler code in practice, go ahead and check your box. If you would only approve one version but not the other, and that doesn't depend on the practical consideration of which one produces simpler code, you may want to raise a separate concern for that.
@rfcbot merge @rfcbot concern ref
Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:
Concerns:
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.
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
?