Filtering and mapping: design problem

I'm working on a command line tool to process my data. The workflow is easy, the program:

  • loads some files and creates structs, that are stored in a Vec<_>
  • there are some filters, that define which struct to process
  • there are also actions, that are to be applied to structs selected with filters
    Both filters and actions are controlled by command-line options.

So far I implemented that in a straightforward way, say:

if args.if_condition_1 {
  filtered = input.iter().filter(|&s| s.chars().next().unwrap().is_uppercase()).collect();
} 
if args.if_condition_2 {
  // other filter here
}
// ... than a similar block for mapping

but the more filters and actions I implement, the more cluttered is my code. Ideally, I'd like to store my filters and actions in closures. In Python that would look like:

input = ["Zebra", "nice camel", "horse"]


filter = lambda s: True   #     Default filter is always True
# Process command line flags to see what filter to apply
if args.is_uppercase:
    filter = lambda s: s[0].isupper()

action = lambda s: s   #     Default action makes nothing
# Process command line flags to see what action we do this time
if args.make_uppercase:
    action = lambda s: s.upper()

for s in input:
    if filter(s):
        print(action(s))

I've tried to use closures, e.g.:

let mut filter = |s: &String| { true };
if args.is_uppercase { filter = |s: &String| { s.chars().next().unwrap().is_uppercase() }; }

, but I've only learned that:

6 |     if true { filter = |s: &String| { s.chars().next().unwrap().is_uppercase() }; }
  |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
  |
  = note: expected closure `[closure@filter_action.rs:4:22: 4:34]`
             found closure `[closure@filter_action.rs:6:24: 6:36]`
  = note: no two closures, even if identical, have the same type
  = help: consider boxing your closure and/or using it as a trait object

Can this be achieved with closures or do I have to implement a hierarchy of objects of some trait?

You can do this pretty easily by boxing dynamically dispatched callables:

#[allow(non_upper_case_globals)]
const is_uppercase: bool = true;

fn main() {
    let mut filter: Box<dyn Fn(&str) -> bool> = Box::new(|_| true);

    if is_uppercase {
        filter = Box::new(|s| s.chars().next().unwrap().is_uppercase());
    }
    
    let input = vec!["Zebra", "nice camel", "horse"];
    
    for s in input.iter() {
        if filter(s) {
            println!("- {s:?}");
        }
    }
}

A much simplier solution would be to just make your args part of the filter/map

let filter = |s: &String| {
    if args.is_uppercase {
        return s.chars().next().unwrap().is_uppercase();
    }
    true
};
2 Likes

That's probably unnecessary; non-capturing closures can coerce directly to fn pointers, and even if they capture, they can coerce to &mut dyn FnMut(…) -> bool if they are only used locally.

1 Like

True, but you just know the very next question would involve why a capturing closure doesn't work with a fn pointer. :slight_smile:

Actually, my very next question is how to chain such filters and actions? E.g. in Python I'd say:

input = ["Zebra", "nice camel", "Horse"]

for f in [filter1, filter2]:
    input = [x for x in input if f(x)]

You can similarly collect everything into a Vec eagerly, that's what the Python code above does.

If you want to re-assign filtered iterators, then either you must make sure their types match exactly, or convert them to effectively dynamically-typed trait objects (dyn Iterator).

But a better solution would probably be to simply apply the conjunction of each filter function in a single filtering step.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.