doplumi
doplumi

Reputation: 3118

How do I reduce multiple if branches to one in order to avoid duplicating similar logic?

A database API gives us the option to build a Filter object that will be passed onto a Query.

It offers a fluent API to build such Filter:

filter
  .topic0(topic0)
  .topic1(topic1)
  .topic2(topic2)
  .topic3(topic3)

The topics are pre-organised into a Vec<Option<String>>, which we can pass onto the filters obj, using the helper function:

fn add_topics(mut f: &Filter, topics: Vec<String>) {
    if let Some(topic0) = topics.get(0) {
        f = &f.topic0(topic0);
    }

    if let Some(topic1) = topics.get(1) {
        f = &f.topic1(topic1);
    }

    if let Some(topic2) = topics.get(2) {
        f = &f.topic2(topic2);
    }

    if let Some(topic3) = topics.get(3) {
        f = &f.topic3(topic3);
    }
}

Unfortunately, the db API doesn't offer a .topics() method that accepts a Vec<_>.

Still, is there any way to avoid duplication of the logic?

E.g. I'm proficient with JS/TS, in which the above can be written as:

const addTopics = (filter: Filter, topics: string[]) {
  for (let i = 0; i < topics.length; ++i) {
    if (topics[i]) {
      filter = filter['topic' + i](topics[i]);
    }
  }
}

Rust, as a typed language, doesn't allow this as far as I know.

Is there any other way?

Upvotes: 0

Views: 123

Answers (1)

Rob Napier
Rob Napier

Reputation: 299703

By a minimal reproducible example, Herohtar means something like:

struct Filter {}

impl Filter {
    fn topic0(&self, topic: &str) -> &Self {
        self
    }
    fn topic1(&self, topic: &str) -> &Self {
        self
    }
    fn topic2(&self, topic: &str) -> &Self {
        self
    }
    fn topic3(&self, topic: &str) -> &Self {
        self
    }
}

fn main() {
    let topic0 = "";
    let topic1 = "";
    let topic2 = "";
    let topic3 = "";

    let filter = Filter {};
    let f = filter
        .topic0(topic0)
        .topic1(topic1)
        .topic2(topic2)
        .topic3(topic3);
}

This abstracts away all of the details, while giving a clear example of what you mean. (If what I've written isn't what you mean, then that's the point of writing a minimal example.)

To build the function you're looking for, you need a way to map what method you want to call to each location. That's fairly straightforward, although it requires a somewhat advanced type:

fn add_topics(mut f: &Filter, topics: Vec<Option<String>>) {
    // Fancy type because of the borrows that need a lifetime.
    // It's just the signature for a method call. The `for` allows
    // adding a lifetime.
    type TopicAssigner = for<'a> fn(&'a Filter, &str) -> &'a Filter;

    // Now, match up the methods in the order of the Vector
    let assign_topic: Vec<TopicAssigner> = vec![
        Filter::topic0,
        Filter::topic1,
        Filter::topic2,
        Filter::topic3,
    ];

    // And zip it with what you're passed
    for (topic, assigner) in zip(topics, assign_topic) {
        if let Some(topic) = topic {
            f = (assigner)(f, &topic);
        }
    }
}

Note that this has a problem if topics has more values than are expected, so it would be nice to force it into a 4-element array instead:

fn add_topics(mut f: &Filter, topics: [Option<String>; 4]) {

But perhaps that's inconvenient for other reasons.

Upvotes: 2

Related Questions