Closed WaffleLapkin closed 3 months ago
Note that this proposed opt.is_none_or(..)
can already be written as opt.is_none() || opt.is_some_and(..)
.
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.
Note that this proposed
opt.is_none_or(..)
can already be written asopt.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.
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.
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
.
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)?
I had the opposite experience. Once I started using
is_some_and
I realized I didn't missis_none_or
at all, because all those cases were easily handled by!is_some_and
.
As an example of that:
I quickly looked at https://github.com/rust-lang/rust/pull/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.
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: https://github.com/rust-lang/rust/pull/73040#pullrequestreview-442096330.
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 problemsResult::is_ok_or_err_and
doesn't seem appealing@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.
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.
@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...
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(!))
Rrright. I'll check/fix the PR more closely and see if there are still valid use cases.
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
.
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?
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?
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.
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 https://github.com/rust-lang/rfcs/pull/3573 which is a language feature which provides similar functionality.
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 theOption::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
Links and related work
There is an open PR implementing this, that was untouched for 8 moths: https://github.com/rust-lang/rust/pull/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.