rust-lang / rust

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

Tracking Issue for Option::is_some_and and Result::is_{ok,err}_and #93050

Closed m-ou-se closed 1 year ago

m-ou-se commented 2 years ago

Feature gate: #![feature(is_some_and)]

This is a tracking issue for Option::is_some_and, Result::is_ok_and and Result::is_err_and.

Public API

impl<T> Option<T> {
    pub fn is_some_and(self, f: impl FnOnce(T) -> bool) -> bool;
}

impl<T, E> Result<T, E> {
    pub fn is_ok_and(self, f: impl FnOnce(T) -> bool) -> bool;
    pub fn is_err_and(self, f: impl FnOnce(E) -> bool) -> bool;
}

Steps / History

Unresolved Questions

the8472 commented 2 years ago

What are the use-cases? And won't that largely be covered by if-let chains?

if let Some(foo) = opt && foo > bar {

}
m-ou-se commented 2 years ago

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 };
the8472 commented 2 years ago

I assume deadline_passedwould 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.

camsteffen commented 2 years ago

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.

m-ou-se commented 2 years ago

I assume deadline_passedwould 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.

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 ifs, 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 bools 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.

m-ou-se commented 2 years ago

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.

jakoschiko commented 2 years ago

The name is_some_and would allow us to add its counterpart is_none_or in the future.

m-ou-se commented 2 years ago

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.

camsteffen commented 2 years ago

FWIW this was attempted in #75298.

jam1garner commented 2 years ago

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

filip-hejsek commented 2 years ago

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.

AmitPr commented 2 years ago

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(());
}
m-ou-se commented 2 years ago

@AmitPr if last == Some(next) should also work, right?

SrTobi commented 2 years ago

@m-ou-se: not if next is expensive.

m-ou-se commented 2 years ago

It seems like everybody is happy with the current names for these methods. I think it's time to consider stabilization.

@rfcbot merge

rfcbot commented 2 years ago

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.

SrTobi commented 2 years ago

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.

m-ou-se commented 2 years ago

is_none_or is a separate method that has been suggested but isn't part of this tracking issue.

SrTobi commented 2 years ago

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.

camsteffen commented 2 years ago

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.

m-ou-se commented 2 years ago

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.

m-ou-se commented 2 years ago

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

camsteffen commented 2 years ago

I think functions named is_ shouldn't consume self.

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.

RalfJung commented 2 years ago

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.

RalfJung commented 2 years ago

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.

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.

camelid commented 2 years ago

Seems like the feature gate name needs to be updated as well (is_some_withis_some_and).

m-ou-se commented 2 years ago

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. :)

m-ou-se commented 2 years ago

@rfcbot cancel

rfcbot commented 2 years ago

@m-ou-se proposal cancelled.

joshtriplett commented 2 years ago

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.

camelid commented 2 years ago

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.

Xiretza commented 2 years ago

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.

camelid commented 2 years ago

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.

MatrixDev commented 2 years ago

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.

RalfJung commented 2 years ago

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.

MatrixDev commented 2 years ago

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.

RalfJung commented 2 years ago

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.)

MatrixDev commented 2 years ago

@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.

RalfJung commented 2 years ago

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?

SabrinaJewson commented 2 years ago

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).

camsteffen commented 2 years ago

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.

BurntSushi commented 2 years ago

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.

jplatte commented 2 years ago

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.

joshtriplett commented 2 years ago

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.

SrTobi commented 2 years ago

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.

joshtriplett commented 2 years ago

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.

scottmcm commented 2 years ago

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.

joshtriplett commented 2 years ago

@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.

joshtriplett commented 2 years ago

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

rfcbot commented 2 years ago

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.