[Pre-RFC] filter_map for Option


#1
  • Feature Name: option_filter_map
  • Start Date: 2018-11-30
  • RFC PR:
  • Rust Issue:

Summary

This RFC proposes the addition of Option::filter_map to combine the functionality of Option::filter and Option::map.

Motivation

Given the following optional collection of optional items, we would like to write a function that gives us the n-th element, or None:

fn main() {
    let things: Option<Vec<Option<u8>>> = Some(vec![Some(1), None, Some(3)]);
    assert_eq!(get_elem(&things, 0), Some(1));
    assert_eq!(get_elem(&things, 1), None);
    assert_eq!(get_elem(&things, 2), Some(3));
}

fn get_elem(coll: &Option<Vec<Option<u8>>>, n: usize) -> Option<u8> {
    // ...
}

Using only the existing Option::map function, we can cover the case where the n-th element is a Some(_) perfectly well, but for handling the None case we can either panic, use a magic value or return Some(None), none of which is satisfactory:

// assume that vec[n] == None
coll.as_ref().map(|vec| vec[n].unwrap()) // this panics
coll.as_ref().map(|vec| vec[n].unwrap_or(42)) // this returns 42
coll.as_ref().map(|vec| vec[n]) // this returns Some(None)

We can solve this by checking for the vec[n] == None situation beforehand using Option::filter:

fn get_elem(coll: &Option<Vec<Option<u8>>>, n: usize) -> Option<u8> {
    coll.as_ref()
        .filter(|vec| vec[n].is_some())
        .map(|vec| vec[n].unwrap())
}

This works, but is needlessly convoluted and requires looking up the n-th element twice. This could be combined into a single Option::filter_map call, which could be used like this:

fn get_elem(coll: &Option<Vec<Option<u8>>>, n: usize) -> Option<u8> {
    coll.as_ref().filter_map(|vec| vec[n])
}

And implemented like this:

pub trait OptionFilterMap<T> {
    fn filter_map<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U>;
}

impl<T> OptionFilterMap<T> for Option<T> {
    fn filter_map<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> {
        match self {
            Some(x) => f(x),
            None => None
        }
    }
}

Rationale and alternatives

This would simplify a common use case. An alternate but more generic solution is already possible:

coll.as_ref().map_or(None, |vec| vec[n])

The proposed new function would be equivalent to using Option::map_or with None as the default value.

The rationale for including this as a separate function anyway is discoverability and functional similarity to Iterator::filter_map and its special case Iterator::find_map.


#2

I haven’t read the details, but I like consistent HKTs, and Maybe should be a monad in the same way List is :wink:


#3

This might be my lack of PL design knowledge, but I’m not sure what this means, especially in the context of my proposal.


#4

The more I think about this, the more I’m unsure if this would really add a lot of value, especially given the existence of Option::map_or.

On the other hand, I stand by my rationale of discoverability, further illustrated by me only stumbling across the suitability of Option::map_or for this scenario while writing up this draft RFC. :man_shrugging:


#5

Isn’t this just the existing Option::and_then?


#6

Well, I’ll be… Yes, that seems to do exactly what my proposed method does. Thank you for pointing this out.

I must have missed this function because of its naming. Iterator has filter and map and combines them into filter_map. Option has filter and map and combines them into… and_then.


#7

Come to think of it, this idea came up before when someone proposed Option::filter: https://github.com/rust-lang/rfcs/pull/2124#issuecomment-325047426


#8

Maybe rustdoc should support adding aliases for better discoverability?

impl Option {
    #[doc(alias = "filter_map")]
    pub fn and_then(...)
}

And, searching issues for it I just ran across the unstable doc_alias feature. It looks like this only affects search, whereas I was thinking about something that also adds entries to the method list on the page. Also appears to just be an internal attribute for a specific std usecase rather than something that’s intended to be stabilized at some point.


#9

Operation can be seen as the monad bind operator. A monad is a wrapper for your values that knows how to take care of the combining of the wrapper parts - so you can ignore them and focus on the value parts. It comes from thinking like “we can do 2 + 3, wouldn’t it be great if we could do Some(2) + Some(3), or Ok(2) + Ok(3) and it just work” (though this is a functor, it’s this kind of “forget the container” logic).

The monad for Option or Result is saying “wouldn’t it be great if we have this sequence of fail-able operations, and whenever we get an error we just return early that error”.

So in haskell we write

do
  x <- checkedAdd 2 3
  y <- checkedAdd x 3
  return y -- This is totally different from return in procedural 
           -- languages, it just kind of does something similar.

and the equivalent in rust

let x = 2.checked_add(3)?;
let y = x.checked_add(3)?;
Ok(y)

but the haskell version is more general - it works with anything like this (futures, etc.).

Hope this makes sense - I always struggle to explain monads, and am not sure I understand them fully


#10

Having an alias in the documentation would maybe have helped me, yes.

Even better would be if RLS were to support aliases so you can find them when coding. (Or even having an actual function alias, but that would probably bring too many drawbacks of its own to be worthwhile.)