Addressing a trait implementation without instantiating it

While I landed in this forum quite a number of times when googling, this is my first post. So: Hello forum!

As part of a larger application I implemented a trait with several implementations (currently 2, expected to be 50+). Each implementation represents a certain strategy on how to proceed with the application. Users can choose at runtime which strategy to use, so hardcoding isn't an option. This is what I have so far:


impl dyn Strategy {
  pub fn allocate_strategy(name: &str) -> Result<Box<dyn Strategy>, String> {
    match name {
      "Follow" => Ok(Box::new(Follow {})),
      "Moving" => Ok(Box::new(Moving {})),
      _ => Err(format!("No strategy for '{}' found", name)),
    }
  }

  pub fn get_parameter_set(name: &str) -> Result<Vec<String>, String> {
    match name {
      "Follow" => Ok(Follow::get_parameter_set()),
      "Moving" => Ok(Moving::get_parameter_set()),
      _ => Err(format!("No strategy for '{}' found", name)),
    }
  }
}

pub trait Strategy {
  fn recommendation(&self) -> Recommendation;
}

Follow and Moving are implementations of trait Strategy. All this compiles and works fine.

What's itching me, of course, is that duplicate matching code in allocate_strategy() and get_parameter_set(). String name comes from the GUI and I have to re-implement the match for every function not being a method (having self as first parameter), just because I can't address the particular implementation somehow.

This is how it could look like, but is missing a few bits:

impl dyn Strategy {
  fn find_strategy(name: &str) -> Result<?????, String> {
    match name {
      "Follow" => Ok(Follow),
      "Moving" => Ok(Moving),
      _ => Err(format!("No strategy for '{}' found", name)),
    }
  }

  pub fn allocate_strategy(name: &str) -> Result<Box<dyn Strategy>, String> {
    let strategy = Strategy::find_strategy(name)?;
    Ok(Box::new(strategy {}))
  }
[...]

Any ideas on how to solve this, how to de-duplicate the match?

2 Likes

It's not clear what role get_parameter_set() is to play (it doesn't appear in your second example), so I can't offer any advice on how to solve that part of the problem.

You can avoid potentially inconsistent string interpretation by defining an enum:

enum StrategyKind {
    Follow,
    Moving,
}

Then you can parse it from a string in only one place (and strum or serde can define that parsing for you), and all other matches can be exhaustive on the enum.

2 Likes

If you need the result of get_parameter_set() to depend on the strategy dynamically, then why don't you make it a method? After all, it looks like that's exactly what you need: to once instantiate a strategy, then call its methods.

1 Like

Because I need the parameter set for creating a working instance. This parameter set is sent to the GUI (web browser via HTTP), there the user can make its choices, then trigger a request to run the strategy with these parameters. Perhaps I simplified code for this forum post a bit too much.

Of course I could instantiate a dysfunctional/empty Strategy just to run get_parameter_set(), but this wouldn't be elegant either.

Now trying to tackle the challenge with an enum, as @kpreid suggested, which will take a while. Thanks for the answers so far, much appreciated!

Welcome to the forums @Traumflug!

Sorry if it's anti-climactic though, because there's no way to return a bare type like you desire.
You can only return instances of types.

The Real Answer

I agree with @kpreid, It seems like what you really want here would be an enum.

With traits, it's impossible to ever have a fully exhaustive list of all the possible implementations,
so your approach of matching over every possible name isn't feasible.

For instance, I could make my own struct that implements this trait, but it would be useless in practice, since it's name wouldn't be in your special list.

Macro Answer

If you really want the exact structure you described, you can achieve this with a macro that generates the match statements for you. This way you only have to physically type out the match once in a single place (the macro), and the compiler will 'copy' it into each function for you.

See this playground for a fully working example.

But again, you should probably try using an enum before reaching for a macro like this : v)

1 Like

I think you are confused about the responsibilities and trying to abstract too much.

If you need the parameters for creating an instance, then that should be the only method where you match on the name, and from that point onward, you will be able to use the strategy. Thus, the other matches become unnecessary.

2 Likes

Then I'd have to make the strategy persistent across multiple HTTP requests, which is quite error prone. This kind of client <-> server protocol used to be quite popular (e.g. PHP Sessions), but was eventually replaced with the much more robust REST architectural style.

This means, every request is self-sufficient. Whatever gets allocated to serve a request gets thrown away at the end of the request. Every request contains everything needed to serve it. One can even stop and restart the server (here: the Rust application) between a request for the parameter set and a request to run a strategy.

I don't get why you would need to make anything persistent for this.

I think the idea is that the strategy name string and the Vec<String> parameter set are serializable data that can be persisted in a session, then converted into the concrete impl Strategy in each request where it should be actually used.

In that case I would say that my previous suggestion of using an enum is probably best — enums are great for adding a little type-checking over strings, and can be trivially serialized whereas trait objects would need even more work.

2 Likes

That doesn't contradict anything I wrote though, does it? It doesn't really matter if your "strongly-typed" strategy is implemented as an enum or as a fixed set of structs hidden behind a dyn Trait. Probably the enum is a little easier to serialize/deserialize, as you mentioned, but this is of little relevance to what the question seems to be.

With both solutions properly implemented, the error-prone matching on the name string would occur only once. In an enum-based solution, some matching would have to take place again in all methods, but it would be type-safe, whereas using dynamic dispatch, no further matching would take place. This requires in either case the dependent methods to take a self parameter, because they need the dynamic, run-time information stored in the strategy object in order to proceed.

The HTTP request for getting the parameter set and the request for running the strategy are two distinct requests. It works like this:

  1. The browser shows a list of available strategies using HTML and JavaScript. This list is currently hardcoded in HTML.
  2. User selects one of these strategies.
  3. Triggered by this selection, JavaScript sends a HTTP request with some JSON to the server (Rust application) along the lines of "User has just choosen strategy 'Moving', which parameters should I display?"
  4. Rust calls Moving::get_parameter_set(), this is where the first match happens. It then encodes the result as JSON and answers the request with this. Let's say the answer is a: number, b: bool, c: number.
  5. Browser/JavaScript parses this JSON and builds HTML for each required parameter.
  6. User adjusts these parameters to its liking and clicks the 'Run' button.
  7. This triggers yet another request back to the Rust application. This times along the lines of "User wants to run strategy 'Moving' using these parameters: a = 5, b = true, c = 99."
  8. Rust code sees this and acts accordingly. This is where the second match happens.

Each of these two types of requests are entirely independent. A user could well change the strategy 5 times, triggering the getParameterSet request 5 times, before choosing to actually run one of these strategies.

This is probably the key point in this quest. One can return instances and one can return functions (as closures), but one can't return or somehow store a bare type or implementation pointer.

I tried the enum route and ended up with this:

pub enum StrategyKind {
  Follow,
  Moving,
}

impl dyn Strategy {
  pub fn find_strategy(name: &str)
  -> Result<StrategyKind, String> {
    match name {
      "Follow" => Ok(Follow),
      "Moving" => Ok(Moving),
      _ => Err(format!("No strategy for '{}' found", name)),
    }
  }

  pub fn allocate_strategy(name: &str)
  -> Result<Box<dyn Strategy>, String> {
    let strategy = <dyn Strategy>::find_strategy(name)?;
    match strategy {
      Follow => Ok(Box::new(Follow {})),
      Moving => Ok(Box::new(Moving {})),
    }
  }
[...]

This de-duplicated the match against strings. Enum Follow has the same name as trait implementation Follow here, but they aren't the same thing, so it requires yet another enum, just a slightly simpler one. Not really better.

Eventually I gave up and made get_parameter_set() a method:

impl dyn Strategy {
  pub fn find_strategy(name: &str)
  -> Result<Box<dyn Strategy>, String> {
    match name {
      "Follow" => Ok(Box::new(Follow::empty())),
      "Moving" => Ok(Box::new(Moving::empty())),
      _ => Err(format!("No strategy for '{}' found", name)),
    }
  }

  // A bit pointless in this simplified form :-)
  pub fn allocate_strategy(name: &str)
  -> Result<Box<dyn Strategy>, String> {
    Self::find_strategy(name)
  }
}

pub trait Strategy {
  fn get_parameter_set(&self) -> Vec<String>;
  fn recommendation(&self) -> Recommendation;
}

Not too elegant, allocates some 100 unused bytes and requires some extra code to distinguish between a strategy complete enough to be used and one only good enough for running get_parameter_set(), still it works fine and is probably the best one can get.

Thanks everybody for helping! I learned a lot today.

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.