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::contains` and `Result::contains` #62358

Closed Centril closed 1 year ago

Centril commented 5 years ago

This is a tracking issue for Option::contains and Result::contains.

The implementations are as follows:


    pub fn contains(&self, x: &T) -> bool where T: PartialEq {
        match self {
            Some(y) => y == x,
            None => false,
        }
    }

    pub fn contains(&self, x: &T) -> bool where T: PartialEq {
        match self {
            Ok(y) => y == x,
            Err(_) => false,
        }
    }
``
camsteffen commented 3 years ago

"equals" is just a simpler concept than "contains" IMO.

  1. "this contains a value equal to x"
  2. "this equals Some(x)"

Edit: I'm not entirely against contains, I just think eq_some is a little better, and allows for a similar eq_err.

camsteffen commented 3 years ago

@camsteffen So it would be eq_slice, eq_string, eq_vector too?

No?

Rustinante commented 3 years ago

@soc if the Option is for an underlying container class, it will easily lead to bugs because the one might accidentally confuse the .contains for the Option and the .contains for the underlying container. If you have two kids, why give them the same name?

Rustinante commented 3 years ago

Option is also different from the container classes. It participates in pattern matching in a special way in Rust and denote the presence or absence of values, while container classes don't. Similarly, the Result class has a special place in Rust, it denotes success/failure. I hope we do not conflate them with the container classes.

camsteffen commented 3 years ago

Why do we have is_none instead of is_empty? That would be more consistent. Because Option is special. eq_some, eq_ok and eq_err are consistent with each other so that's worth something.

tvercruyssen commented 3 years ago

So we make a PR to rename all the Option and Result methods to eq_some, eq_ok and eq_err and reorder them maybe too ? Or are we still bikesheding names?

camsteffen commented 3 years ago

I think a lang team decision is needed at this point, if arguments have been exhausted.

Rustinante commented 3 years ago

What’s a conjecture? And I’m not sure I get the point of comparing a specific function name in a different language, does Scala have a function called is_none too?

Rustinante commented 3 years ago

Also, speaking of regrets, yes i’m already regretting that we picked the contains name because of the reasons that were mentioned earlier in this thread, the confusion caused by the conflation of Option and containers was what led me to this GitHub issue in the first place. So I think there’s no need to look for further evidence of regrets in other languages…

glittershark commented 3 years ago

to put a little weight on one side of the discussion (though it might not be needed now) I actually went looking for Option::contains in my autocomplete, and was really happy to find it there, but disappointed to see that it was unstable. I'm pretty sure I'd never go looking for a function called eq_some - or any of the other names that have been proposed here. I know I'm just one person, but just wanted to provide a "field report" data-point of sorts in favor of the principle of least surprise.

glittershark commented 3 years ago

Also, rust already considers Option a container (as well it should) - look no further than Option::iter() and Option::into_iter()

Rustinante commented 3 years ago

Being able to iterate and being a "container" in the real sense are not the same thing. We can have a "container" that contains all the rational numbers but there won't be a natural way to iterate through it. On the other hand, being able to iterate through a generator of all the positive integers doesn't make the generator a "container".

It's true that nothing prevents us from making the Option a "container", but Option is a special language construct, as it participates pattern matching and is therefore more like an enum. Having different names such as eq_some would serve to remind us that we're dealing with an optional value, because, as I've mentioned before, it's very easy to confuse calling the contains on the underlying object in case it's a container, and calling the contains on the Option itself. And my field report would be that such confusions can easily lead to bugs.

jhpratt commented 3 years ago

@Rustinante I'm not sure if you're aware given your wording, but Option is an enum. There is nothing special about it with regard to pattern matching. Option isn't even a lang item, it just has some special casing for diagnostic purposes.

Rustinante commented 3 years ago

I’m talking more in the context of functional programming, Option has a long tradition in those functional languages. Do you do pattern matching on vectors?

Rustinante commented 3 years ago

And borrowing the unequivocal statement from @jhpratt , if we so unequivocally state that Options IS an enum, then the question is settled that Option shouldn’t be treated as a container, since making something both an enum and a container would probably just be confusing

robjtede commented 3 years ago

+1 on @glittershark's annecdote. I too have typed .contains and found it in the auto-complete dropdown, knowing immediately that it will do what I want (followed swiftly by disappointment that it is unstable).

It should probably be said that I did not stop to consider the nature of the Option container and it's facets as a psudeo-container type. It is simply a natural name for a method that checks some form of inner equality. I can not see this being a problem or otherwise confusing in the code I write day-to-day.

ShadowJonathan commented 3 years ago

I'll give some 2c regarding mental acrobatics as to why ::contains is sensible here; Option::Some is a container, it "contains" the value T in itself, even though the container can also not contain it's value, with contains then you ask the question "contains this container value X?" or "does this container contain value X?"

Some more 2c: even if this makes more sense to be eq after all, like @glittershark said, the muscle memory of contains is already there, so I'd argue this is more "missing" than anything else.

Lastly, I would personally find thing.contains(x) to be more readable than thing == Some(x) in long if-chains.

Rustinante commented 3 years ago

@ShadowJonathan im not sure if you realized but you just said Option::Some is a container, not Option itself

Rustinante commented 3 years ago

@soc I don't understand what you're trying to say. Nobody said anything about special "compiler" treatment. We have listed good reasons and not grasping for straws. Anybody interested please read the entire thread. There's no point arguing in a circle here. All the points I've seen this past week have been some "autocomplete" argument, and some assertions about .contains won't ever cause problems, and some statements that Option is already a "container" (which i had been trying to show why it is different from)

Rustinante commented 3 years ago

By Option being a special language construct, i'm talking about Some/None in the functional programming sense, not whether it's a library or compiler treatment. The point being, as I have already said before, that Option is different from other containers. If you like to look at other languages so much, see the Some and None in Ocaml and you'll see what i mean by Option being different from vectors from a conceptual point of view.

For those that assert that it won't ever confusions, please do us all a favor and read the thread from the beginning, https://github.com/rust-lang/rust/issues/62358#issuecomment-676127715 etc.

And please let's have better reasons than citing "autocomplete"

jhpratt commented 3 years ago

To everyone: please remember to be respectful. I foresee things heading downhill quite quickly and would prefer that not be the case.

Rustinante commented 3 years ago

20 years of combined evidence of what? In a different language there's a different context. We have listed several examples where confusions had shown up, those are already examples that show that it's not a non-problem.

I think addressing those confusions shown by the examples is more important than what autocomplete does.

enterprisey commented 3 years ago

Assume you have a function getFoo() returning Option<String> then it might be misleading if you do getFoo().contains("bar") since it looks like it checks for a substring when it in fact checks for an exact match. Would some other name perhaps be more clear?

I was convinced by this argument, then I realized it also works for Vec<String>. (Minus some type inference shenanigans, see #42671.)

kaleidawave commented 3 years ago

Don't think this is mentioned in this thread but a "contains" method would be useful in situations in scenarios for result where E doesn't implement PartialEq. At the moment my_result == Ok(42) only works if my_result::E: PartialEq. I use a current my_result.ok() == Some(42) workaround occasionally (and there is the more verbose if let Ok(v) = my_result { v == 42 } else { false }). I think that my_result.contains(42) (or whatever the method name will be) has benefits over using the equality operator in this scenario.

jhpratt commented 3 years ago

@soc This change has nothing to do with the edition in the first place.

m-ou-se commented 2 years ago

This method might be more useful if it took a predicate:

option.contains(|c| c.is_lowercase())

For most cases, == Some(x) and == Ok(x) suffices can be used for checking (partial) equality, but checking whether an Option contains something fulfilling some predicate gets verbose or non-trivial:

let now: Time = ..;
let deadline: Option<Time> = ..;

// 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);

With a predicate-based .contains:

let g = deadline.contains(|d| d > now);

It wouldn't match the .contains() function we already have on slices and collections, but it would match str's .contains() which can also take a predicate. I don't think consistency with slices is very important though. It's nice that Option and Result can be used with .into_iter() as a collection of 0 or 1 elements, but I don't think they need to match the full slice/collection interface.

glittershark commented 2 years ago

What if that was called .any, to match Iterator::any?

eddyb commented 2 years ago

What if that was called .any, to match Iterator::any?

The problem is that is that .any/.all imply generality over "arbitrary numbers of elements" in a way other methods Option and Iterator share (.map, .filter, etc.) generally don't.

Closest equivalents I can think of would be .is or .has, as singular counterparts for "all are" / "all have".

camsteffen commented 2 years ago

I don't have any qualms with str::contains(Pattern), but I do fear it might be problematic to use it to justify Option::contains with a predicate. In my mind, the name str::contains makes sense primarily because it accepts &str or char, and the char predicate is an added convenience that avoids adding another method. This would be the first contains method that does not accept a value to compare against and I think that would be perplexing (though not terribly so).

That said, I agree that the predicate version seems more generally useful. So I think it would be good to pursue that with higher precedence than the PartialEq version.

Some ideas: some_and, and_is, is, where, having, and_thusly<-joke

For most cases, == Some(x) and == Ok(x) suffices can be used for checking (partial) equality,

So does the predicate use case even overlap with the PartialEq use case? It seems like there is theoretically room for two functions here. But again, I'd pursue the predicate function first and see where that leaves us.

ShadowJonathan commented 2 years ago

I personally like and_is or some_is the most, and for the predicate option, i'd like to suggest and_with/some_with.

(Though something in the back of my mind tells me _with was for with a return value generic, not bool returns)

m-ou-se commented 2 years ago

It seems like there is theoretically room for two functions here.

Maybe, but each function will need a clear name. And I'd really like to use contains for .contains(|x| x > 5) rather than .contains(&5), since I think that the latter will rarely be useful (especially for Options).

We tend to have a relatively high bar for additions to fundamental things like Option and Iterator. I personally think .contains(&5) does not pass that bar, but .contains(|x| x > 5) does.

(Side note: I also think the existing .contains() for slices was a mistake, and should've taken a predicate too. But at least there's .iter().any(..), which for slices is quite obvious and readable. (Unlike for Options and Results.))

m-ou-se commented 2 years ago

I personally like and_is or some_is the most, and for the predicate option, i'd like to suggest and_with/some_with.

If I look at these three lines of code

let a = optional_char.contains(|c| c.is_lowercase());
let b = optional_char.and_with(|c| c.is_lowercase());
let c = optional_char.some_with(|c| c.is_lowercase());

then as an reader without context I'd probably only understand the first one without having to look up the documentation to see what the method does.

and_with makes me think of and_then, which is quite different. And some_with reminds me of bool::then_some, and sounds like it'll produce a Some rather than a boolean.

ShadowJonathan commented 2 years ago

Good points, though still, when i personally look at (answer as i32).contains(42) i more or less expect the X contains Y english "logic" format, rather than X contains (test with Y) as answer.contains(|x| x == 42) would be.

An alternative (that i forgot to write down) was and_test, or just test, but that might touch the entire idea of rust's integrated testing, so i wasn't 100% sure about it.

(Also sorry if this bikeshedding is not warranted at the moment)

Edit: and_if, following up from the comment below

jhpratt commented 2 years ago

In my opinion anything that accepts a closure here should have "if" in the name.

camsteffen commented 2 years ago

and_if complements and_then nicely.

opt.contains(|c| c.is_lowercase())
opt.and_if(|c| c.is_lowercase())
res.ok().and_if(|c| c.is_lowercase())
opt.some_and(|c| c.is_lowercase())
res.ok_and(|c| c.is_lowercase())
ShadowJonathan commented 2 years ago

I dont think that any bikeshedding of unrelated topics have come up, this is still primarily about the naming of contains specifically, afaik.

gorsat commented 2 years ago

Such a useful and intuitive feature with such a straightforward implementation yet it's been stuck here for 2 years? Wow...

As someone who is new to Rust I expected this feature to exist for Option, and I happily and quickly found it in the docs, but then discovered I'd have to switch to unstable to use it. Now I come here and find it has been stuck in a naming debate for literally 2+ years. That's really unfortunate. Doesn't the Rust community have a way to push through this log jam?

univerz commented 2 years ago

i'm happily using nightly feature, contains + cointains_with for lambda case looks intuitive & matches behaviour of other _with apis.

emi2k01 commented 2 years ago

Someone mentioned test but they dismissed it in the same comment but I think it's a good one.

opt.test(|n| n > 10) res.test(|n| n == 20) res.test_err(|e| e == ErrorKind::NotFound)

The word "test" is already used to describe similar methods like Iterator::any and Iterator::all that take a closure and return a boolean. It is also clear what is doing.

I think any of the already mentioned names by other people are clear enough and most people should understand what the method does given the context on where it appears.

if file.metadata().method(Metadata::is_file) {
    std::fs::copy(&from, &to)
}

if string.parse_url().method(|url| url.has_host()) {
    urls.push(string);
}

Replace method with any of the suggested names above and any programmer would know what the code is doing.

I really doubt the name of these functions are worth discussing for more than two years.

pmnoxx commented 2 years ago

Someone mentioned test but they dismissed it in the same comment but I think it's a good one.

opt.test(|n| n > 10) res.test(|n| n == 20) res.test_err(|e| e == ErrorKind::NotFound)

The word "test" is already used to describe similar methods like Iterator::any and Iterator::all that take a closure and return a boolean. It is also clear what is doing.

I think any of the already mentioned names by other people are clear enough and most people should understand what the method does given the context on where it appears.

if file.metadata().method(Metadata::is_file) {
    std::fs::copy(&from, &to)
}

if string.parse_url().method(|url| url.has_host()) {
    urls.push(string);
}

Replace method with any of the suggested names above and any programmer would know what the code is doing.

I really doubt the name of these functions are worth discussing for more than two years.

How about using the word any?

Right now, you can use the word any on iterator

   [1,2,3].iter().any(|x| x == 2)

This also works on iterators from options:

   Some(2).iter().any(|x| x == 2)

Using the same name for option, directly, would be more discoverable.

   Some(2).any(|x| x == 2)

When I started thinking about it. Option<...> is just an iterator for at most one element.

ShadowJonathan commented 2 years ago

When I started thinking about it. Option<...> is just an iterator for at most one element.

It's been noted earlier in the thread that there are both stances of looking at Option as an iterator, and looking at it as it's own thing/Result-ish, just fyi.

pmnoxx commented 2 years ago

When I started thinking about it. Option<...> is just an iterator for at most one element.

It's been noted earlier in the thread that there are both stances of looking at Option as an iterator, and looking at it as it's own thing/Result-ish, just fyi.

In my opinion current apis for Option / Result, Iter<Val> / Iter<Result<Val, Error> are a bit inconsistent. I found out that I have to learn separate naming for all of those types above. Methods even though they do the same thing ,they are called differently.

I've thought a bit, on how we could make an easy to remember api, that works for both options, results, iterators, iterators of results. Here is what I came up with map

      /// Vec<Result<...>>
        let mut vec: Vec<Result<u32, Error>> = Vec::new();

        // modifies Ok(...)value  // doesn't exist  yet
        let x = vec.iter().map_ok(|x| x).collect_vec();
        // modifies Err(...)value  // doesn't exist yet
        let x = vec.iter().map_err(|e| e).collect_vec();
        // this modifier the value itself // that's how it currently works
        let x = vec.iter().map(|v| v).collect_vec();

        /// Result
        let mut x: Result<u32, Error> = Ok(5);

        // modifies Ok(...)value  // doesn't exist  yet
        x.map_ok(|ok|ok) 
        // modifies Err(...)value  // exists, works as expected
        x.map_err(|e}e) 
        // Should modify  the result // exists, but modifies ok value
        x.map(|r||r) 

      /// Vector of values
        let mut vec: Vec<u32> = Vec::new();
      // map exists; works as expected
        let x = vec.iter().map(|v| v).collect_vec();

        let mut x: Option<u32> = Some(5);
        // maps value // exists; works as expected
       x.map(|x| x * 5)

My argument would be to have similar api for any, just make it work with any, any_ok, any_err, just as seen above. I really, don't want to spend time learning 3 types of api names for options / results, iter, and also not having good Iter<Result<...>> options.

I found out white processing values in iterators that there is no good error handling.

I really would like to write something like this, for processing network queries.

fn url_query(id: usize) -> Result<UrlData, Error>()

(0...1000)
.map(|id|url_query(id)) // now current data isResult<UrlData, Error>()
.map_err(|e|pretty_print(e))
.map_ok(|v|process(v))
.collect()

Currently I would have to do the following

(0...1000)
.map(|id|url_query(id)) // now current data isResult<UrlData, Error>()
.map(|v|{
   if let Err(e) = &v{
       pretty_print(e))
   }
   v
}
.map(|v|{
   if let Some(val) = &v{
       Some(process(val))
   } else {
      v
   }
}
.collect()
mbartelsm commented 2 years ago

I have to say I agree with @pmnoxx comment. Currently, the APIs for Option, Result, and Iter are all over the place. You can argue that they are not the same and should not be the same, but in practice, they all serve and are used as Monads whose inner values can be modified in consistent ways (albeit, with inconsistent APIs).

As a relatively new user, one of the most annoying parts of Rust is having to go back and forth between code and documentation for types whose behavior ought to be the same.

In that regard, I think using closures and naming this any would be the most consistent approach. It might impose a small barrier of entry for learning what any does since it might not be immediately apparent, but it has the overwhelming benefit that you only need to learn the method name once for three of the most common types/traits in the std lib.

m-ou-se commented 2 years ago

How about using the word any?

I disagree with trying to make Option look like an iterator. We already have a way to view an Option as an iterator: option.into_iter(), which gives a std::option::IntoIter: an Option wrapper with an Iterator interface. While Option is isomorphic with an option::IntoIter, they're separate types on purpose. something.any(f) makes it look like f might be executed multiple times, which isn't the case for a contains/any/test method on Option. (Also note that slices/arrays etc. don't have an .any() method either.)

Someone mentioned test but they dismissed it in the same comment but I think it's a good one.

The name test doesn't make it clear what happens in the None case. For example, in optional_timeout.contains(|t| t > now()) reads to me as "Does it contain a timeout that has passed?", to which the answer is clearly no/false in the case of None. This is not as clear with a less meaningful name like test.

m-ou-se commented 2 years ago

@rust-lang/libs-api Any opinions on the discussion starting at https://github.com/rust-lang/rust/issues/62358#issuecomment-970280621 ?

BurntSushi commented 2 years ago

Briefly, for me, I'll say that I am not a fan of using any here. Largely for the same reasons pointed out by @m-ou-se earlier. It seems like a highly confusable name to me.

I also think whatever routine we settle on should probably accept a predicate. I think a routine that accepts a value based on PartialEq isn't nearly as useful.

I'm not a huge fan of the name contains, but I think it fits. It's also hard for me to think of a better name. I somewhat like some_is, ok_is and err_is, where is is in my experience strongly associated with "predicate." But the names don't feel quite right to me for reasons that I'm finding difficult to articulate.

I think for me, with respect to contains, I do find the near consistency we have with that API in std to be appealing. It is true that str::contains is polymorphic and can accept a predicate, but I think every other use of contains accepts a value of some sort, and not a predicate. That does seem like a nice consistency to preserve and codify if possible.

mbartelsm commented 2 years ago

While I did advocate for using any instead of contains, I do want to say that I find it more important to have this method with some name than to not have it, for the reasons that @m-ou-se points out in their comment. It's verbose to write with the current stable methods and many of the ways to write it obscure legibility.

ShadowJonathan commented 2 years ago

One last name suggestion; has_with, where has alludes to the "contains", and with implies a predicate.

Rustinante commented 2 years ago

@BurntSushi what do you think about the alternatives that were brought up earlier, eq_some, eq_err?

yaahc commented 2 years ago

I mentioned this in the libs team meeting but I'll reiterate here. I personally don't agree that deadline.map(|d| d > now).unwrap_or(false) looks verbose or confusing, and I prefer it over deadline.contains(|d| d > now).

I'm trying to think through why this is, and as an exercise I thought I'd try imagine how I'd read these expressions if they were part of an if statement. In the process I realized I wouldn't read the former as sentence, and would instead want to read it as it's own boolean producing expression that I would then use in the if. So if I were to use these in practice I think I would write slightly different code.

// kinda had to guess at what this snippet was representing
let deadline_is_active = deadline.map(|d| d > now).unwrap_or(false);
if deadline_is_active {
    // ...
}

vs

if deadline.contains(|d| d > now) {
    // ...
}

This latter one on the other hand I do feel comfortable reading directly, and I'd read this as "if deadline contains some instant later than now".

Admittedly, one could still outline and name the condition in the second example the same as the first, and the difference would be substantially reduced, but I do think that the former snippet is more likely to encourage this style of separation. My conclusion is that you're absolutely right @m-ou-se that contains would be less verbose, but I'm still not at all convinced that it reduces confusion.