★ Factories and Strategies in Rust, Deref on Box
— 4 minutes read
Let say, the users of a CLI tool should be able to choose different implementation (strategies) on how to solve a specific task by using corresponding CLI parameters. In this case, the CLI tool has to select the strategies dynamically during the runtime according to the user’s selection. One way to do that is to use define a interface (trait) and different implementations. A factory parses the user’s input and returns the corresponding strategy. The main part of the CLI tool then does not need to care about the selected strategy.
Rust prefers static dispatch for polymorphism using generics. This means that for a given trait and its implementations, the compiler tries to determine which concrete method of the concrete implementation (type) to call during compilation time by looking at the involved types. This happens during compilation times. For all variants, the compiler generates specialized functions on the concrete used types. But since this happens during compilation time and the decision which strategy to use happens during runtime, this approach cannot be easily used in Rust.
So one way to solve this is to use dynamic dispatch using vtables like this: The factory returns a boxed type and the algorithm takes a boxed type as input. algorithm2
works, too, and supports the generics way two. So if the concrete type is known during compilation, monomorphisation can be used.
One important note regarding the unusual syntax &*Box<_>
. In order to keep a clean API algorithm
takes an &dyn Strategy
. Box
implements Deref
so you could assume that it’s enough to pass &s1_box
to algorithm
. But this is not the case, because for the compiler does not know, if you want &Box<_>
or a reference ot the unboxed value — see below. Therefore, &*
does the trick.
#[derive(Debug)]
pub struct Stuff {
val: usize,
}
pub trait Strategy {
fn do_stuff(&self) -> Stuff;
}
// Dynamic dispatch, ploymorphisation => compiler will emit only one function and the code will use a vtable to find the right method to call.
pub fn algorithm(strategy: &dyn Strategy) -> Stuff {
strategy.do_stuff()
}
// Dynamic dispatch, ploymorphisation => compiler will emit only one function and the code will use a vtable to find the right method to call.
// Static dispatch, monomorphisation => compiler will emit two functions. One for
// algorithm2(S1) and one for algorithm2(S2)
// => The compiler will emit _3_ versions of the method, i.e., one for dynamic dispatch and two
// for static dispatch
// Use rustc --emit llvm-ir and look for `; main::factory::algorithm2` in the resulting file
pub fn algorithm2<T: Strategy + ?Sized>(strategy: &T) -> Stuff {
strategy.do_stuff()
}
pub struct S1 {}
impl Strategy for S1 {
fn do_stuff(&self) -> Stuff {
Stuff { val: 1 }
}
}
pub struct S2 {}
impl Strategy for S2 {
fn do_stuff(&self) -> Stuff {
Stuff { val: 2 }
}
}
pub fn factory(s: usize) -> Box<dyn Strategy> {
match s {
1 => Box::new(S1 {}),
2 => Box::new(S2 {}),
_ => panic!("No such strategy"),
}
}
pub fn main() {
let s1_box: Box<Strategy> = factory(1);
let s1 = S1 {};
let _ = algorithm(&*s1_box); // dyn dispatch; compiler does not known what concret type is in the Box.
let _ = algorithm(&s1); // dyn dispatch; compiler knows the exact type but the method only takes trait objects
let _ = algorithm2(&*s1_box); // dyn dispatch; compiler does not known what concret type is in the Box.
let _ = algorithm2(&s1); // static dispatch; compiler knows the exact type and the method takes concrete types
let s2_box: Box<Strategy> = factory(2);
let s2 = S2 {};
let _ = algorithm(&*s2_box); // dyn dispatch; compiler does not known what concret type is in the Box.
let _ = algorithm(&s2); // dyn dispatch; compiler knows the exact type but the method only takes trait objects
let _ = algorithm2(&*s2_box); // dyn dispatch; compiler does not known what concret type is in the Box.
let _ = algorithm2(&s2); // static dispatch; compiler knows the exact type and the method takes concrete types
let s3: Box<Strategy> = factory(2);
let s3_ref: &dyn Strategy = &*s3;
let _ = algorithm(s3_ref); // dyn dispatch; compiler does not known what concret type is in the Box.
let _ = algorithm2(s3_ref); // dyn dispatch; compiler does not known what concret type is in the Box.
}
Fur further reading see
- A Quick Look at Trait Objects in Rust
- Funny interaction between deref coersion and
Vec<Box<Trait>>
— This explains theBox
deref issue. - Casting &Box
to &dyn Trait — … and how to fix it here. - Statically-dispatched strategy using traits? — This might be a way for an even more general way to use strategies.