Create iterator that relies on temporary values

Hello! I was trying to build an iterator incrementally but I found myself in a really intricate case and I'm not sure how to make the code less if-else heavy, here an example of what I'm trying to do:

#[derive(PartialEq, Eq, Debug)]
pub enum TypeA {
    A,
    B,
    C,
}
#[derive(PartialEq, Eq, Debug)]
pub enum TypeB {
    BA,
    BB,
    BC,
}

#[derive(Debug)]
pub struct Example {
    a: TypeA,
    b: TypeB,
}

impl Example {
    fn new(a: TypeA, b: TypeB) -> Self {
        Self { a, b }
    }
}

pub struct CreatureSearchBuilder {
    pub values: Vec<Example>,
    pub a: Option<TypeA>,
    pub b: Option<TypeB>,
}

impl CreatureSearchBuilder {
    pub fn new(values: Vec<Example>) -> Self {
        Self {
            values,
            a: None,
            b: None,
        }
    }

    pub fn search(&self) -> Vec<&Example> {
        if let Some(a) = &self.a {
            if let Some(b) = &self.b {
                self.values
                    .iter()
                    .filter(|v| v.a == *a && v.b == *b)
                    .collect()
            } else {
                self.values.iter().filter(|v| v.a == *a).collect()
            }
        } else if let Some(b) = &self.b {
            self.values.iter().filter(|v| v.b == *b).collect()
        } else {
            vec![]
        }
    }
}

pub fn main() {
    let values = vec![
        Example::new(TypeA::A, TypeB::BA),
        Example::new(TypeA::B, TypeB::BB),
        Example::new(TypeA::C, TypeB::BC),
    ];
    let mut search = CreatureSearchBuilder::new(values);
    search.b = Some(TypeB::BB);
    let results = search.search();
    println!("{:?}", results);
}

The central point here is the search function, I have to duplicate the code multiple times to filter and collect and adding new filter values this is going to be worst and worst with many if else statements.

This is the way I was trying to write it, using a variable to keep track of the "new" iterator:

let mut iter = values.iter();

if let Some(t) = test_1 {
    iter = iter.filter(|v| v.a = t);
}
if let Some(t) = test_2 {
    iter = iter.filter(|v| v.b = t);
}

iter.collect()

but I was unlucky with the approach even if it was the best way I can think of because of the temporary values (test_1 and test_2) that will be used by the collect function at the end but dropped after the if block are finished.

I tried to box the predicate but that was unlucky as well and still ugly considering that I still have to have the if statements.

Is there any other way I can write that search function with the approach I described?

Thank you :slight_smile:

You can just move the checks inside the call to filter instead of trying to chain calls to filter

pub fn search(&self) -> Vec<&Example> {
    if self.a.is_none() && self.b.is_none() {
        return Vec::new();
    }

    self.values
        .iter()
        .filter(|v| {
            let mut result = true;

            if let Some(a) = &self.a {
                result &= v.a == *a;
            }

            if let Some(b) = &self.b {
                result &= v.b == *b
            }

            result
        })
        .collect()
}
1 Like

Thanks for it, not sure how I could not see that.
Just as curiosity, will this not make the application slower because of the ifs for each element?

After inlining, it's very likely that the optimizer is able to hoist the ifs outside the loop. And even if not, the CPU's branch prediction should make them almost free.

2 Likes

Oops, I misunderstood the code... Most of this post was wrong.

Note that to assign iter.filter() to iter again you would have to box it, which inhibits inlining and is probably far worse than a stray branch.

2 Likes

Thanks everybody very useful info :slight_smile: