2

Given a situation where we receive inputs for some nodes type like 'nodeA' or 'nodeB', and we want to initialize structs with that same input. Is it possible without a gigantic switch block? These structs share similar behaviour (using a Trait for that) but with some differences.

pub trait Executable {
    fn run(&self);
}

pub struct NodeA {}

impl Executable for NodeA {
    fn run(&self) {}
}

pub struct NodeB {}

impl Executable for NodeB {
    fn run(&self) {}
}

Flow:

User inputs 'nodeA'

Program initializes struct nodeA with some data

User inputs 'nodeB'

Program initializes struct nodeB with some data

...

To specify better, the final use case is reading a JSON file with all the nodes and respective params to be instantiated. Some of those nodes can come from external plugins, so the number of existing nodes can become very big.

11
  • There are many ways to initialize data. Will every NodeA be constructed the same way as every other NodeA? If so, you just need an associated method that will set it up, and your match statement doesn't have to do anything besides delegate a constructor. It's hard to give specific advice without knowing your use case. Commented Jun 30, 2022 at 16:52
  • So I guess there will be many nodes? Otherwise I don't see the problem with a match - case, or how you call it, a switch block (which is how it's called in most other languages). Commented Jun 30, 2022 at 16:57
  • @JeremyMeadows Yap, the final use case it that there will be lots of nodes, some of them coming from external plugins (a future use case), so a match - case block would not scale well. Commented Jun 30, 2022 at 16:58
  • If your JSON has a set schema, you should use the serde crate to parse it. That will automatically populate all the data, the only thing you'd have to maintain is a single struct that matched your JSON layout. Commented Jun 30, 2022 at 17:03
  • The only alternative that I know would be a registry type thing, combined with a factory pattern. Commented Jun 30, 2022 at 17:13

1 Answer 1

2

For smaller, static number of nodes, I think a match - case construct is perfectly fine.

But if you have a larger number of nodes, or the available nodes is dynamically changing, I would implement something like this:

pub trait Executable {
    fn run(&self);
}

pub struct NodeA {}

impl Executable for NodeA {
    fn run(&self) {
        println!("NodeA::run()");
    }
}

pub struct NodeB {}

impl Executable for NodeB {
    fn run(&self) {
        println!("NodeB::run()");
    }
}

pub trait Matcher {
    fn try_match(&self, s: &str) -> Option<Box<dyn Executable>>;
}

pub struct NodeAMatcher;
pub struct NodeBMatcher;

impl Matcher for NodeAMatcher {
    fn try_match(&self, s: &str) -> Option<Box<dyn Executable>> {
        (s == "NodeA").then(|| Box::new(NodeA {}) as Box<dyn Executable>)
    }
}
impl Matcher for NodeBMatcher {
    fn try_match(&self, s: &str) -> Option<Box<dyn Executable>> {
        (s == "NodeB").then(|| Box::new(NodeB {}) as Box<dyn Executable>)
    }
}

struct MatcherRegistry {
    matchers: Vec<Box<dyn Matcher>>,
}

impl MatcherRegistry {
    fn new() -> Self {
        Self { matchers: vec![] }
    }
    fn register_matcher(&mut self, matcher: impl Matcher + 'static) {
        self.matchers.push(Box::new(matcher));
    }
    fn try_get_node(&self, s: &str) -> Option<Box<dyn Executable>> {
        self.matchers
            .iter()
            .filter_map(|matcher| matcher.try_match(s))
            .next()
    }

    fn try_execute(&self, s: &str) {
        if let Some(node) = self.try_get_node(s) {
            node.run();
        } else {
            println!("'{}' not found.", s);
        }
    }
}

fn main() {
    let mut registry = MatcherRegistry::new();

    registry.register_matcher(NodeAMatcher);
    registry.register_matcher(NodeBMatcher);

    registry.try_execute("NodeA");
    registry.try_execute("NodeB");
    registry.try_execute("NodeC");
}
NodeA::run()
NodeB::run()
'NodeC' not found.

Here, you have a factory pattern.

The structs NodeAMatcher and NodeBMatcher are factories for NodeA and NodeB. They can check if the input matches, and then create an Executable object.

Then, you collect all possible factories (or Matchers here) in a registry, here called MatcherRegistry. You can then, at runtime, add or remove matchers as you wish.


Of course, if you don't need to create a new object every time and the act of executing doesn't consume it, you can reduce the complexity a little by bypassing the factory pattern:

use std::collections::HashMap;

pub trait Executable {
    fn run(&self);
}

pub struct NodeA {}

impl Executable for NodeA {
    fn run(&self) {
        println!("NodeA::run()");
    }
}

pub struct NodeB {}

impl Executable for NodeB {
    fn run(&self) {
        println!("NodeB::run()");
    }
}

struct ExecutableRegistry {
    executables: HashMap<&'static str, Box<dyn Executable>>,
}

impl ExecutableRegistry {
    fn new() -> Self {
        Self {
            executables: HashMap::new(),
        }
    }
    fn register_executable(
        &mut self,
        command: &'static str,
        executable: impl Executable + 'static,
    ) {
        self.executables.insert(command, Box::new(executable));
    }

    fn try_execute(&self, s: &str) {
        if let Some(node) = self.executables.get(s) {
            node.run();
        } else {
            println!("'{}' not found.", s);
        }
    }
}

fn main() {
    let mut registry = ExecutableRegistry::new();

    registry.register_executable("NodeA", NodeA {});
    registry.register_executable("NodeB", NodeB {});

    registry.try_execute("NodeA");
    registry.try_execute("NodeB");
    registry.try_execute("NodeC");
}

Of course there exists a large mount of other variations of the same patterns. Which one you implement is up to you and your usecase.

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you, I was trying to implement something similar with the HashMap but could not figure out that logic related to the Box. Makes sense since we don't know at runtime the space that it will take. Thank you again, will experiment a variation of this
@lowkey_daisy at *compile time ;)
Oops, my bad :D
Instead of using a new factory struct I would just use a closure coerced to a function pointer and store HashMap<&'static str, fn() -> Box<dyn Executable>>, or even Vec<(&'static str, fn() -> Box<dyn Executable>)>.
@ChayimFriedman Can fn have data attached to it? Like, a capturing closure? Because to my knowledge fn can only be static function pointers, which would not be compatible with his use case of loading it at runtime. Although HashMap<&'static str, Box<dyn Fn() -> Box<dyn Executable>>> would probably work.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.