Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option::is_none_or #212

Closed
WaffleLapkin opened this issue Apr 18, 2023 · 18 comments
Closed

Option::is_none_or #212

WaffleLapkin opened this issue Apr 18, 2023 · 18 comments
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api

Comments

@WaffleLapkin
Copy link
Member

Proposal

Problem statement

It seems like there is a common use-case to check if an option is None or some condition holds for its value.

Motivation, use-cases

Similarly to how we have Option::is_some_and which is basically .map_or(false, ...), sometimes it is desirable to have the opposite "default" value, i.e. sometimes a better name for .map_or(true, ...) is wanted. See for example comments on the Option::is_some_and tracking issue:

See also 45 occurrences of .map_or(true, ...) in the rustc itself:

list

:~/rust-lib (rust-lib); rg ".map_or\(true" ./compiler --stats -q | rg "\d+ matches"
45 matches
:~/rust-lib (rust-lib); rg ".map_or\(true" ./compiler
./compiler/rustc_middle/src/values.rs
180:                let check_params = def_id.as_local().map_or(true, |def_id| {

./compiler/rustc_middle/src/ty/instance.rs
236:            return ty.ty_adt_def().map_or(true, |adt_def| {

./compiler/rustc_session/src/parse.rs
58:        self.spans.borrow().get(&feature).map_or(true, |spans| spans.is_empty())

./compiler/rustc_passes/src/dead.rs
682:                .map_or(true, |layout| layout.is_zst())

./compiler/rustc_hir_analysis/src/coherence/inherent_impls_overlap.rs
152:        .map_or(true, |overlap| {

./compiler/rustc_codegen_cranelift/build_system/build_sysroot.rs
72:            if file.extension().map_or(true, |ext| ext.to_str().unwrap() != "o") {

./compiler/rustc_hir_analysis/src/check_unused.rs
79:            tcx.extern_mod_stmt_cnum(def_id).map_or(true, |cnum| {

./compiler/rustc_attr/src/builtin.rs
1092:                if sess.opts.pretty.map_or(true, |pp| pp.needs_analysis()) {

./compiler/rustc_resolve/src/macros.rs
840:            if kind != NonMacroAttrKind::Tool && binding.map_or(true, |b| b.is_import()) {

./compiler/rustc_resolve/src/imports.rs
249:                || max_vis.get().map_or(true, |max_vis| vis.is_at_least(max_vis, self))

./compiler/rustc_parse/src/parser/attr.rs
428:        attr.ident().map_or(true, |ident| {

./compiler/rustc_resolve/src/lib.rs
1066:        if def_id.map_or(true, |def_id| def_id.is_local()) {

./compiler/rustc_resolve/src/diagnostics.rs
285:            self.extern_prelude.get(&ident).map_or(true, |entry| entry.introduced_by_item);
512:                if filter_fn(res) && ctxt.map_or(true, |ctxt| ctxt == key.ident.span.ctxt()) {

./compiler/rustc_trait_selection/src/traits/error_reporting/suggestions.rs
1463:                    .map_or(true, |def_id| self.tcx.object_safety_violations(def_id).is_empty())
1499:                        && last_ty.map_or(true, |last_ty| {

./compiler/rustc_infer/src/infer/mod.rs
1474:            value.as_ref().map_or(true, |value| !value.needs_infer()),

./compiler/rustc_mir_dataflow/src/framework/graphviz.rs
416:        assert!(befores.as_ref().map_or(true, ExactSizeIterator::is_empty));

./compiler/rustc_mir_dataflow/src/framework/direction.rs
290:                    if dead_unwinds.map_or(true, |dead| !dead.contains(bb)) {
505:                    if dead_unwinds.map_or(true, |dead| !dead.contains(bb)) {
537:                    if dead_unwinds.map_or(true, |dead| !dead.contains(bb)) {
563:                    if dead_unwinds.map_or(true, |dead| !dead.contains(bb)) {

./compiler/rustc_hir_typeck/src/_match.rs
155:                                && prior_arm.map_or(true, |(_, t, _)| self.can_coerce(t, ret_ty))

./compiler/rustc_hir_typeck/src/lib.rs
490:    trait_did.map_or(true, |trait_did| {

./compiler/rustc_hir_typeck/src/expr.rs
1458:        if count.try_eval_usize(tcx, self.param_env).map_or(true, |len| len > 1) {

./compiler/rustc_hir_typeck/src/coercion.rs
947:                    .map_or(true, |u| u.is_empty()) =>

./compiler/rustc_hir_typeck/src/fn_ctxt/_impl.rs
992:                .map_or(true, |ty| expected.peel_refs() != ty.peel_refs())

./compiler/rustc_hir_typeck/src/generator_interior/mod.rs
396:            || ty.map_or(true, |ty| {

./compiler/rustc_hir_typeck/src/fn_ctxt/checks.rs
2190:                .filter(|(idx, _)| expected_idx.map_or(true, |expected_idx| expected_idx == *idx))

./compiler/rustc_hir/src/def.rs
729:        self.ns().map_or(true, |actual_ns| actual_ns == ns)

./compiler/rustc_index/src/interval.rs
234:        current.map_or(true, |x| x < self.domain as u32)

./compiler/rustc_expand/src/config.rs
466:        parse_cfg(&meta_item, &self.sess).map_or(true, |meta_item| {
474:        if !self.features.map_or(true, |features| features.stmt_expr_attributes) {

./compiler/rustc_const_eval/src/interpret/util.rs
44:                        let is_used = unused_params.contains(index).map_or(true, |unused| !unused);

./compiler/rustc_const_eval/src/interpret/intern.rs
117:        let frozen = ty.map_or(true, |ty| ty.is_freeze(*ecx.tcx, ecx.param_env));

./compiler/rustc_const_eval/src/interpret/memory.rs
431:                if offset.checked_add(size, &self.tcx).map_or(true, |end| end > alloc_size) {

./compiler/rustc_const_eval/src/interpret/projection.rs
295:            if from.checked_add(to).map_or(true, |to| to > len) {

./compiler/rustc_mir_transform/src/const_prop_lint.rs
722:                            .map_or(true, |layout| layout.is_zst())

./compiler/rustc_mir_transform/src/coverage/graph.rs
387:            if !self.counter_kind.as_ref().map_or(true, |c| c.is_expression()) {

./compiler/rustc_mir_transform/src/const_prop.rs
1142:                            .map_or(true, |layout| layout.is_zst())

./compiler/rustc_mir_transform/src/coverage/spans.rs
484:            if self.prev_expn_span.map_or(true, |prev_expn_span| {

./compiler/rustc_mir_build/src/check_unsafety.rs
172:            ref kind if ExprCategory::of(kind).map_or(true, |cat| cat == ExprCategory::Place) => {

./compiler/rustc_borrowck/src/type_check/mod.rs
1892:                if len.try_eval_usize(tcx, self.param_env).map_or(true, |len| len > 1) {

./compiler/rustc_lint/src/expect.rs
29:                && tool_filter.map_or(true, |filter| expectation.lint_tool == Some(filter))

./compiler/rustc_ast_passes/src/feature_gate.rs
100:                if self.sess.opts.pretty.map_or(true, |ppm| ppm.needs_hir()) {

Solution sketches

impl<T> Option<T> {
    pub fn is_none_or(self, f: impl FnOnce(T) -> bool) -> bool {
        match self {
            None => true,
            Some(x) => f(x),
        }
    }
}

Links and related work

There is an open PR implementing this, that was untouched for 8 moths: rust-lang/rust#100602

What happens now?

This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.

@WaffleLapkin WaffleLapkin added api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api labels Apr 18, 2023
@m-ou-se
Copy link
Member

m-ou-se commented May 19, 2023

Note that this proposed opt.is_none_or(..) can already be written as opt.is_none() || opt.is_some_and(..).

@m-ou-se
Copy link
Member

m-ou-se commented May 19, 2023

This was discussed in the libs meetup today. We generally have a relatively high bar for methods on types like Option and Result, and we don't think this passes that bar considering the alternatives available.

@WaffleLapkin
Copy link
Member Author

Note that this proposed opt.is_none_or(..) can already be written as opt.is_none() || opt.is_some_and(..).

Note that this only works if opt is a variable. For something like f().is_none_or(..) your suggestion won't work.

.map_or(true, ..) is an option, but it (same as your suggestion) is (imho) much less readable.

@CryZe
Copy link

CryZe commented Jun 2, 2023

With is_some_and stabilized, I started replacing all the occurrences of map_or(false, in my codebase. It turns out there's about just as many cases of map_or(true, to the point where it feels really weird that there's no is_none_or, so I would like this to be reconsidered.

@m-ou-se
Copy link
Member

m-ou-se commented Jun 5, 2023

I had the opposite experience. Once I started using is_some_and I realized I didn't miss is_none_or at all, because all those cases were easily handled by !is_some_and.

@m-ou-se
Copy link
Member

m-ou-se commented Jun 5, 2023

If we were to have a Option::is_none_or method, would you also expect a Result::is_err_or (that runs a closure taking the Ok value) and Result::is_ok_or (that takes a closure taking the Err value) and a Result::is_ok_or_err_and (that takes both a closure for the Ok value and one for the Err value)?

@m-ou-se
Copy link
Member

m-ou-se commented Jun 5, 2023

I had the opposite experience. Once I started using is_some_and I realized I didn't miss is_none_or at all, because all those cases were easily handled by !is_some_and.

As an example of that:

I quickly looked at rust-lang/rust#111873 for use cases, and the very first file in that change is:

        let is_macro_callsite = self
            .session
            .source_map()
            .span_to_snippet(span)
-           .map(|snippet| snippet.starts_with("#["))
-           .unwrap_or(true);
+           .map_or(true, |snippet| snippet.starts_with("#["));

Which seems like an argument for is_none_or, but then the next line is:

        if !is_macro_callsite {

Which makes me think that it'd actually be easier to follow to use is_some_and and remove the !, such that it basically says: "if the snippet is available and doesn't start with #[", which I'd find easier to understand.

@WaffleLapkin
Copy link
Member Author

WaffleLapkin commented Jun 5, 2023

Result::is_err_or and Result::is_ok_or seem potentially interesting, I think I saw a few cases in the compiler where they can be used.

The naming is a problem though, is_err_or looks too much like is_error, similarly to Option::err_or which was not accepted a while back: rust-lang/rust#73040 (review).

For Result::is_ok_or_err_and I haven't yet seen any use-cases yet. It is pretty uncommon, although not unheard of, to have Result<T, T>, so usefulness of this method (especially with such a long name...) is questionable. Additionally it can be expressed via matches!(x, Ok(v) | Err(v) if ...) which is not too bad — it's a bit longer, but perfectly readable.

So, my opinions:

  • Result::is_err_or and Result::is_ok_or may be interesting, but on the verge of being too much as they are far less common (in my experience) and have naming problems
  • Result::is_ok_or_err_and doesn't seem appealing

@WaffleLapkin
Copy link
Member Author

@m-ou-se yeah, this is a bad example, I agree 😅

Ig a better example would be something like

pub fn is_ungated(&self, feature: Symbol) -> bool {
-    self.spans.borrow().get(&feature).map_or(true, |spans| spans.is_empty())
+    self.spans.borrow().get(&feature).is_none_or(|spans| spans.is_empty())
}

As using is_some_and would require double-negation:

    !self.spans.borrow().get(&feature).is_some_and(|spans| !spans.is_empty())

Similarly I think !(...).is_some_and() is not nice with long chains, where you have to remember negation from the start.

@the8472
Copy link
Member

the8472 commented Jun 5, 2023

Imo the Option/Result/Iterator/Stream/TryStream methods are a clusterfuck. In some cases it's flat_map or and_then or then. If "these are needed for consistency/to fill in quadrants in a feature matrix" is the argument then they should actually be consistent so understanding carries from one to the other.

In the current situation where each gets a bespoke name I end up not remembering the names and having to look them up quite often or relying on RA to offer the right thing. So my general stance is to not add any more unless there's a significantly stronger argument than consistency.

@WaffleLapkin
Copy link
Member Author

WaffleLapkin commented Jun 5, 2023

@the8472 my main argument in this case is readability, all other options seem significantly less readable than is_none_or when it can be applied directly (i.e. negating its result is not readable, but otherwise it is better).
I also get your annoyance with flat_map/and_then/then, it's very sad...

@m-ou-se
Copy link
Member

m-ou-se commented Jun 5, 2023

Ig a better example would be something like

pub fn is_ungated(&self, feature: Symbol) -> bool {
-    self.spans.borrow().get(&feature).map_or(true, |spans| spans.is_empty())
+    self.spans.borrow().get(&feature).is_none_or(|spans| spans.is_empty())
}

That example also has a negation of the condition. It's hidden in the name: ungated. ;)

I think this would be easier to read:

pub fn is_gated(&self, feature: Symbol) -> bool {
    self.spans.borrow().get(&feature).is_some_and(|spans| !spans.is_empty())
}

"It's gated if there is a non-empty entry for the feature." (is_some_and(!))

versus

"It's ungated if there is no entry for the feature or if the entry is empty." (is_none_or)

or

"It's ungated if there is no non-empty entry for the feature." (!is_some_and(!))

@WaffleLapkin
Copy link
Member Author

Rrright. I'll check/fix the PR more closely and see if there are still valid use cases.

@RalfJung
Copy link
Member

RalfJung commented Nov 12, 2023

What is the bar for reopening this ACP? Here are some recent examples people posted where is_none_or would have made the code more clear:

Generally this is not surprising; is_none_or is the de-Morgan dual to is_some_and after all. We have both && and ||, and we have both and_then and or_else; it is inconsistent with the rest of our API to have only one of the pair of duals for is_some_and and is_none_or.

@marcelchampagne
Copy link

I recognize the importance of maintaining high standards for methods on Option and Result. However, I also believe is_none_or would be a valuable addition and the most consistent with the existing APIs. Specifically, I think @RalfJung's argument above is a strong one and it seems like there are a significant number of people who also share a similar sentiment, indicated by the numerous 👍 reactions it has received over the past four months.

Would it be possible to reconsider this proposal?

@RalfJung
Copy link
Member

RalfJung commented Mar 14, 2024

Another usecase in the compiler:

tys.last().iter().all(|ty| is_very_trivially_sized(**ty))

This is iterating over an Option just to avoid map_or(false, ...), which IMO is quite hard to follow. With is_none_or this could nicely avoid iterators and become

tys.last().is_none_or(|ty| is_very_trivially_sized(**ty))

I can of course do

!tys.last().is_some_and(|ty| !is_very_trivially_sized(**ty))

but that is extremely hard to reason about.

Having is_some_and but not is_none_or is like having any but not all.

Would it be possible to reconsider this proposal?

I think that requires someone to file a new ACP. Do you want to do that?

@Amanieu Amanieu added the I-libs-api-nominated Indicates that an issue has been nominated for discussion during a team meeting. label May 22, 2024
@Amanieu
Copy link
Member

Amanieu commented May 22, 2024

I've also found this to be quite useful in my own code, and found that !is_some_and with a reversed condition is particularly unreadable. I will therefore re-open this ACP and have it re-considered in our next libs-api meeting.

@Amanieu Amanieu reopened this May 22, 2024
@Amanieu
Copy link
Member

Amanieu commented May 28, 2024

We discussed this in the libs-api meeting again today and we're happy to accept it. This is primarily based on the experience reports which show that only relying on is_some_and is insufficient because inverting the conditions can make the code less readable.

With that said, we would also like people to provide feedback on rust-lang/rfcs#3573 which is a language feature which provides similar functionality.

@Amanieu Amanieu closed this as completed May 28, 2024
@Amanieu Amanieu added ACP-accepted API Change Proposal is accepted (seconded with no objections) and removed I-libs-api-nominated Indicates that an issue has been nominated for discussion during a team meeting. labels May 28, 2024
compiler-errors added a commit to compiler-errors/rust that referenced this issue Jun 13, 2024
workingjubilee added a commit to workingjubilee/rustc that referenced this issue Jun 13, 2024
workingjubilee added a commit to workingjubilee/rustc that referenced this issue Jun 13, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Jun 13, 2024
Rollup merge of rust-lang#126328 - RalfJung:is_none_or, r=workingjubilee

Add Option::is_none_or

ACP: rust-lang/libs-team#212
github-actions bot pushed a commit to rust-lang/miri that referenced this issue Jun 17, 2024
bors pushed a commit to rust-lang/rust-analyzer that referenced this issue Jun 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api
Projects
None yet
Development

No branches or pull requests

7 participants