Reza
Reza

Reputation: 1577

Rust compiler complaints about 2 almost identical closures

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?

Upvotes: 0

Views: 257

Answers (2)

pigeonhands
pigeonhands

Reputation: 3414

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.

Upvotes: 1

Thomas
Thomas

Reputation: 181735

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)
        }
    }

Upvotes: 6

Related Questions