-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: Add Option::filter
to the standard library
#2124
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
Merged
Merged
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
61df0c1
First version of the `option-filter` RFC
LukasKalbertodt 169cd2f
Add tiny example to summary, showing how filter() works
LukasKalbertodt a4713f1
Fix typo
LukasKalbertodt 4fc43fb
Change return type to `Self` instead of `Option<T>` and fix typo
LukasKalbertodt 8857fc3
RFC 2124: Add `Option::filter`
dtolnay File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
- Feature Name: option_filter | ||
- Start Date: 2017-08-21 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
Add the method `Option::filter<P>(self, predicate: P) -> Option<T>` to the | ||
standard library. This method makes it possible to easily throw away a `Some` | ||
value depending on a given predicate. The call `opt.filter(p)` is equivalent | ||
to `opt.into_iter().filter(p).next()`. | ||
|
||
```rust | ||
assert_eq!(Some(3).filter(|_| true)), Some(3)); | ||
assert_eq!(Some(3).filter(|_| false)), None); | ||
assert_eq!(None.filter(|_| true), None); | ||
``` | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
The `Option` type has plenty of methods, every single one intended to help the | ||
user write short code dealing with this ubiquitous type. If we would not care | ||
about convenience when dealing with `Option`, the type would not have nearly | ||
as many methods. | ||
|
||
Just like other methods, `filter()` is a useful method in *certain* | ||
situations. While it is not nearly as important as `map()`, it is very handy | ||
in many situations. The feedback on the [corresponding `rfcs`-issue][issue] | ||
clearly shows that many people encountered a situation in which `filter()` | ||
would have been helpful. | ||
|
||
Consider this tiny example: | ||
|
||
```rust | ||
let api_key = std::env::arg("APIKEY").ok() | ||
.filter(|key| key.starts_with("api")); | ||
``` | ||
|
||
Here is another example showing tree traversal with a queue: | ||
|
||
```rust | ||
let mut queue = VecDeque::new(); | ||
queue.push_back(tree.root()); | ||
|
||
// We want to visit all nodes in breadth first search order, but stop | ||
// immediately once we found a leaf node. | ||
while let Some(node) = queue.pop_front().filter(|node| !node.is_leaf()) { | ||
queue.extend(node.children()); | ||
} | ||
``` | ||
|
||
Additionally, adding `filter()` would make the interfaces of `Option` and | ||
`Iterator` more consistent. Both types already shared a handful of methods | ||
with identical names and functions, most importantly `map()`. Adding another | ||
such method would make the whole interface feel more consistent. | ||
|
||
In the following example the programmer can easily swap `nth()` and `filter()` | ||
statements, if they decide they want to allow the `-j` parameter at any | ||
position. | ||
|
||
```rust | ||
let num_threads = std::env::args() | ||
.nth(1) | ||
.filter(|arg| arg.starts_with("-j")) | ||
.and_then(|arg| arg[2..].parse().ok()); | ||
|
||
``` | ||
|
||
`filter()` can be especially useful for integration into existing method- | ||
chains. Here is a slightly more complicated example which is taken from an | ||
existing, real web app's session management. Note that each line introduces a | ||
new reason to reject the session. | ||
|
||
```rust | ||
// Check if there is a session-cookie | ||
let session = cookies.get(SESSION_COOKIE_NAME) | ||
// Try to decode the cookie's value as hexadecimal string | ||
.and_then(|cookie| hex::decode(cookie.value()).ok()) | ||
// Make sure the session id has the correct length | ||
.filter(|session_id| session_id.len() == SESSION_ID_LEN) | ||
// Try to find the session with the given ID in the database | ||
.and_then(|session_id| db.find_session_by_id(session_id)); | ||
``` | ||
|
||
All these examples would be less easy to read without `filter()`. There are | ||
two main ways to achieve something equivalent to `filter(p)`: | ||
|
||
- `opt.into_iter().filter(p).next()`: notably longer and the `next()` feels | ||
semantically wrong. | ||
- `opt.and_then(|v| if p(&v) { Some(v) } else { None })`: notably longer and a | ||
questionable single-line `if-else`. | ||
|
||
|
||
[issue]: https://github.com/rust-lang/rfcs/issues/1485 | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
A possible documentation of the method: | ||
|
||
> ```rust | ||
> fn filter<P>(self, predicate: P) -> Option | ||
> where P: FnOnce(&T) -> bool | ||
> ``` | ||
> | ||
> Returns `None` if the option is `None`, otherwise calls `predicate` with the | ||
> wrapped value and returns: | ||
> | ||
> - `Some(t)` if `predicate` returns `true` (where `t` is the wrapped value), | ||
> and | ||
> - `None` if `predicate` returns `false`. | ||
> | ||
> This function works similar to `Iterator::filter()`. You can imagine the | ||
> `Option<T>` being an iterator over one or zero elements. `filter()` let's | ||
LukasKalbertodt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
> you decide which elements to keep. | ||
> | ||
> # Examples | ||
> | ||
> ```rust | ||
> fn is_even(n: i32) -> bool { | ||
> n % 2 == 0 | ||
> } | ||
> | ||
> assert_eq!(None.filter(is_even), None); | ||
> assert_eq!(Some(3).filter(is_even), None); | ||
> assert_eq!(Some(4).filter(is_even), Some(4)); | ||
> ``` | ||
> | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
It is hopefully sufficiently clear how `filter()` is supposed to work from the | ||
explanations above. Here is one example implementation: | ||
|
||
```rust | ||
impl<T> Option<T> { | ||
pub fn filter<P>(self, predicate: P) -> Option<T> | ||
where P: FnOnce(&T) -> bool | ||
{ | ||
match self { | ||
Some(x) => { | ||
if predicate(&x) { | ||
Some(x) | ||
} else { | ||
None | ||
} | ||
} | ||
None => None, | ||
} | ||
} | ||
} | ||
``` | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
It increases the size of the standard library by a tiny bit. | ||
|
||
# Rationale and Alternatives | ||
[alternatives]: #alternatives | ||
|
||
- Don't do anything. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
### Maybe `filter()` wouldn't be used a lot. | ||
|
||
The feature proposed in this RFC is already implemented in the | ||
[`option-filter` crate][crate]. This crate hasn't been used a lot (only | ||
around 1500 downloads at the time of writing this). Thus, it makes sense to ask whether people would actually use the `filter()` method. However, there | ||
are many other reasons for not using this crate: | ||
|
||
- The programmer doesn't know about the crate | ||
- The programmer knows about the crate, but doesn't want to have too many tiny | ||
dependencies in their project | ||
- The programmer knows about the crate, but they decided it's too much work to | ||
use the crate. | ||
|
||
A simple calculation: using the crate would require around 80 new characters | ||
(`option-filter = "*"` + `extern crate option_filter;` + | ||
`use option_filter::OptionFilterExt;`) in at least 2, probably 3, files. On | ||
the other hand, using the `.and_then()` workaround shown above would only | ||
need 39 more characters than `filter()` and wouldn't require opening other | ||
files. | ||
|
||
According to the assessment of this RFC's author, the mentioned crate is not | ||
used for reasons independently of `filter()`'s usefulness. | ||
|
||
Reading the comments and looking at the feedback in [this thread][rfcs-issue], | ||
it's clear that there are at least some people openly requesting this feature. | ||
And to give a specific example: this RFC's author wanted to use `filter()` a | ||
whole lot more often than he used some of the other methods of `Option` (like | ||
`map_or_else()` and `ok_or_else()`). | ||
|
||
|
||
[crate]: https://crates.io/crates/option-filter | ||
[rfcs-issue]: https://github.com/rust-lang/rfcs/issues/1485 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.