0

I have this simple struct with 2 Hashsets:

pub struct IpAddresses {
    pub ipv4s: HashSet<String>,
    pub ipv6s: HashSet<String>,
}

and then a simple function which is supposed to provide an iterator to one of the sets:

  pub fn shared2(&self, ipv6: bool) -> impl Iterator<Item = IpAddr> + '_ {
        
       
        if ipv6 {
            self
            .ipv6s
            .iter()
            .filter_map(|a| IpAddr::from_str(a).ok())
        } else {
            self
            .ipv4s
            .iter()
            .filter_map(|a| IpAddr::from_str(a).ok())
        }
    }

I get the following error with the suggestion to use box:

error[E0308]: `if` and `else` have incompatible types
   --> src/models/ip_address.rs:131:13
    |
125 |   /         if ipv6 {
126 |   |             self
    |  _|_____________-
127 | | |             .ipv6s
128 | | |             .iter()
129 | | |             .filter_map(|a| IpAddr::from_str(a).ok())
    | |_|_____________________________________________________- expected because of this
130 |   |         } else {
131 | / |             self
132 | | |             .ipv4s
133 | | |             .iter()
134 | | |             .filter_map(|a| IpAddr::from_str(a).ok())
    | |_|_____________________________________________________^ expected closure, found a different closure
135 |   |         }
    |   |_________- `if` and `else` have incompatible types
    |
    = note: expected type `FilterMap<std::collections::hash_set::Iter<'_, _>, [closure@src/models/ip_address.rs:129:25: 129:53]>`
             found struct `FilterMap<std::collections::hash_set::Iter<'_, _>, [closure@src/models/ip_address.rs:134:25: 134:53]>`
    = note: no two closures, even if identical, have the same type
    = help: consider boxing your closure and/or using it as a trait object
help: you could change the return type to be a boxed trait object
    |
122 |     pub fn shared2(&self, ipv6: bool) -> Box<dyn Iterator<Item = IpAddr> + '_> {
    |                                          ~~~~~~~                             +
help: if you change the return type to expect trait objects, box the returned expressions
    |
126 ~             Box::new(self
127 |             .shared_ipv6s
128 |             .iter()
129 ~             .filter_map(|a| IpAddr::from_str(a).ok()))
130 |         } else {
131 ~             Box::new(self

Interestingly enough if I copy paste one of the wings into a function, the compiler works fine without any error or need for a Box:

   fn list_shared<'a>(&'a self, items: &'a HashSet<String>) -> impl Iterator<Item = IpAddr> + 'a {
        items
            .iter()
            .filter_map(|a| IpAddr::from_str(a).ok())
         
    }

pub fn shared<'a>(&'a self, ipv6: bool) -> impl Iterator<Item = IpAddr> + 'a {
       
        if ipv6 {
            self.list_shared(&self.ipv6s)
        } else {
            self.list_shared(&self.ipv4s)
        }
    }

As you can see this is a copy-paste of the inner block. Why is this happening? How are those 2 identical blocks not identical in the first instance but just putting them inside a function made them identical?

Reza
  • 1,478
  • 1
  • 15
  • 25
  • 1
    Does this answer your question? [Types of unboxed closures being unique to each](https://stackoverflow.com/questions/27874683/types-of-unboxed-closures-being-unique-to-each) – Chayim Friedman Dec 26 '21 at 09:21

2 Answers2

6

Each closure gets its own, anonymous type. Even though the closures have the same call signature, and even if neither of them borrows anything so no lifetimes are part of the signature, these types are not the same!

Therefore, the generic <F> in the returned FilterMap struct has a different type in each if branch, leading to the error message about trying to return incompatible types.

Note that -> impl Iterator tells the compiler that you're returning some type that implements Iterator, but it has to be statically the same type every time, determined at compile time.

When you extract the filter_map call to a separate function, there is only one closure, hence one type returned from that function. And since this is the same type for both if branches, the problem goes away.

It also goes away if you assign the closure to a variable, because then it's the same type in both cases as well:

    pub fn shared2(&self, ipv6: bool) -> impl Iterator<Item = IpAddr> + '_ {
        let from_str = |a: &String| IpAddr::from_str(a).ok();
        if ipv6 {
            self
            .ipv6s
            .iter()
            .filter_map(from_str)
        } else {
            self
            .ipv4s
            .iter()
            .filter_map(from_str)
        }
    }
Thomas
  • 174,939
  • 50
  • 355
  • 478
  • 2
    Thank you @Thomas, I have read your reply multiple times and yet I can't still get my head around it. When you say each closure gets its own anonymous type, what is that anonymous type? The return type for both wings are the same and the compiler can see that. And why does the compiler assign an anonymous type when the type is obvious? And how does putting 2 anonymous types inside 2 separate boxes makes them identical? A box of i32 is not an identical type to a box of i16. – Reza Dec 26 '21 at 08:54
  • @Reza "what is that anonymous type?" it's the type representing the closure object, each closure gets its own and since it's anoymous, there's no "what". – Masklinn Dec 26 '21 at 10:03
  • "The return type for both wings are the same and the compiler can see that." it's not, since each closure has its own type (and the types are not erased or unified) the adapter types are different, therefore the return types are different. – Masklinn Dec 26 '21 at 10:04
  • 1
    "And how does putting 2 anonymous types inside 2 separate boxes makes them identical? A box of i32 is not an identical type to a box of i16." not sure why you're mentioning boxes here (as this answer doesn't mention it), but the reason is [trait objects](https://doc.rust-lang.org/reference/types/trait-object.html) and type erasure: when you convert a box or reference to a trait object, the concrete type is removed and only the trait's remains, at the possible cost of making dispatch dynamic rather than static. – Masklinn Dec 26 '21 at 10:07
  • @Masklinn, I mentioned Box because that is the solution that the compiler suggested. You can see that in my original post. – Reza Dec 26 '21 at 23:01
1

impl Iterator<Item=IpAddr> is saying 'this function will return a static type that is determined at runtime that conforms to Iterator<Item=IpAddr>.

It is not the same as Box<dyn Iterator<Item=IpAddr> which means any type that conforms to Iterator<Item=IpAddr>.

The reason it dosent work is because each each one of |a| IpAddr::from_str(a).ok()) are diffrent types that are generated by the compiler, and filter_map rerurns a struct that has the iterator of the type FilterMap<I, F>, with F being the type of the function.

You can see the same issue if you move your closures to named functions

fn ipv6_iter(a: &String) -> Option<IpAddr> {
    IpAddr::from_str(a).ok()
}
fn ipv4_iter(a: &String) -> Option<IpAddr>  {
    IpAddr::from_str(a).ok()
}

impl IpAddresses {
    pub fn shared2(&self, ipv6: bool) -> impl Iterator<Item = IpAddr> + '_  {
        if ipv6 {
            self
                .ipv6s
                .iter()
                .filter_map( ipv6_iter)
        } else {
            self
                .ipv4s
                .iter()
                .filter_map( ipv4_iter)
        }
    }
}

In your first branch returns a FilterMap<IpAddresses, ipv6_iter>, but the else branch returns a FilterMap<IpAddresses, ipv4_iter>.

By moving the logic into list_shared, both filter_maps use the same anonymous function to do the mapping, therefore have the same FilterMap type.

Same as using the same static function in each filter_map

fn ip_iter(s: &String) -> Option<IpAddr> {
    IpAddr::from_str(s).ok()
}
impl IpAddresses {
    pub fn shared2(&self, ipv6: bool) -> impl Iterator<Item = IpAddr> + '_  {
        if ipv6 {
            self
                .ipv6s
                .iter()
                .filter_map( ip_iter)
        } else {
            self
                .ipv4s
                .iter()
                .filter_map( ip_iter)
        }
    }
}

So each branch returns a FilterMap<IpAddresses, ip_iter>, therefore impl Iterator<Item=IpAddr> has a single type to resolve to.

pigeonhands
  • 3,066
  • 15
  • 26