I am new to the composition approach with Rust, and I am having a hard time trying to figure out whether I could make my code more efficient / smaller.
Let us assume that I have a base struct BaseStruct:
struct BaseStruct {
values: Vec<i32>,
}
impl BaseStruct {
fn new() -> Self {
Self{values: vec![]}
}
}
Then, I define a AsBase trait to ease the composition process:
/ Trait used to do composition
trait AsBase {
fn as_base(&mut self) -> &mut BaseStruct;
// Note that add_val has a default implementation!
fn add_val(&mut self, val: i32) {
// let us assume that this method has hundreds of lines of code
self.as_base().values.push(val);
}
}
// Note that BaseStruct implements the AsBase trait!
impl AsBase for BaseStruct {
fn as_base(&mut self) -> &mut BaseStruct {
self
}
}
Note that BaseStruct implements the AsBase trait! Otherwise, we couldn't add a value to the vector.
Then, I derive the behavior of the base struct using composition:
// Derived struct and handy functions
struct DerivedStruct {
base: BaseStruct,
}
impl DerivedStruct {
fn new() -> Self {
Self{base: BaseStruct::new()}
}
}
// Derived struct also implements the AsBase trait
impl AsBase for DerivedStruct {
fn as_base(&mut self) -> &mut BaseStruct {
&mut self.base
}
}
So now, I can add values to the inner vector of my derived struct using the trait method!
fn main() {
let mut base = BaseStruct::new();
base.add_val(1);
let mut derived = DerivedStruct::new();
derived.add_val(1); // With composition and AsBase trait, I achieve "inheritance"
}
Here you have a playground with this example.
However, what if the add_val default method was very complex and required hundreds of lines of code? Would Rust generate a different method add_val for every struct implementing the AsBase trait? Or is the compiler smart enough to detect that they can share the same function?
Let me try to be clearer: would this alternative implementation be smaller is size, as it explicitly uses the same method?
// Base struct and handy associated methods
struct BaseStruct {
values: Vec<i32>,
}
impl BaseStruct {
fn new() -> Self {
Self{values: vec![]}
}
fn add_val(&mut self, val: i32) {
// Let us assume that this method is hundreds of lines long
self.values.push(val);
}
}
// Trait used to do composition
trait AsBase {
fn as_base(&mut self) -> &mut BaseStruct;
// Note that add_val has a default implementation!
fn add_val(&mut self, val: i32) {
self.as_base().add_val(val);
}
}
// Note that BaseStruct does NOT implement the AsBase trait to avoid naming collision!
// Derived struct and handy functions
struct DerivedStruct {
base: BaseStruct,
}
impl DerivedStruct {
fn new() -> Self {
Self{base: BaseStruct::new()}
}
}
// Derived struct also implements the AsBase trait
impl AsBase for DerivedStruct {
fn as_base(&mut self) -> &mut BaseStruct {
&mut self.base
}
}
fn main() {
let mut base = BaseStruct::new();
base.add_val(1);
let mut derived = DerivedStruct::new();
derived.add_val(1); // With composition and AsBase trait, I achieve "inheritance"
}
(Also, note that I couldn't implement the AsBase trait for BaseStruct due to naming collisions, I don't know if there is a workaround to avoid this other than changing the names of the methods).
Here you have a playground for the alternative version.
Yes, the compiler will generate a new instance of add_val() for each type. It may collapse them if they use the same machine code, but it will not if they don't. If you want to avoid that, a common way is to define a nested function (see Why does std::fs::write(...) use an inner function?):
fn add_val(&mut self, val: i32) {
fn inner(this: &mut BaseStruct) {
// let us assume that this method has hundreds of lines of code
base.values.push(val);
}
inner(self.as_base());
}
However, what you're doing is not composition. Rather, it's emulating inheritance with composition. When using the composition principle, you should not (usually) have an AsBase trait because you should not treat DerivedStruct as BaseStruct. This is not "is-a" relationship, this is a "has-a" relationship. If a method needs a BaseStruct, pass it a reference to the field directly and let it perform its work.
Related
given this example
struct Outer<T>(*mut T);
impl<T> Outer<T> {
pub fn new(value: &mut T) -> Outer<T> {
Outer(value as *mut T)
}
}
struct Inner(pub i32);
impl Inner {
pub fn do_thing(&self) {
println!("did the thing {}", self.0);
}
}
fn main() {
let outer = Outer::new(Inner(2));
outer.do_thing() // error: do_thing doesnt exist
}
how would i expose the methods of Inner as methods of Outer
im trying to achieve what Box is doing
You have to reborrow the pointer in order to implement Deref (playground):
impl<T: ?Sized> Deref for Outer<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { &*self.0 }
}
}
Answer from before the edit below:
The clean way is to have methods that return the inner type:
impl<T> Outer<T> {
pub fn get(&self) -> &T {
&self.0
}
pub fn get_mut(&mut self) -> &mut T {
&mut self.0
}
pub fn into_inner(self) -> T {
self.0
}
}
This is more useful when the inner type isn't pub (you can just do self.0 for the same effect as all three methods), but is a common pattern that users of rust libraries expect.
The other way is to implement Deref and DerefMut. This is messier because it's a very strong API commitment: you now have all the methods of the inner type added to the outer type unconditionally, and gain any future methods. It also means that any namespace collisions between the wrapper and the target are hard to notice, which is why things like Arc::get_mut are associated functions instead of methods. If you use this, consider changing methods on the wrapper to associated functions (don't take a self parameter).
impl<T> Deref for Outer<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
I'm trying to use a trait object in a hashmap so I can call an update function.
This here is really the code I'm trying to write. I am pretty sure that I can change this code to get this to work the way that I want as-is, but I figured I can turn this into a learning opportunity because the Sized stuff is not quite clear to me.
let mut hm: HashMap<SomeEnum, Box<dyn SomeTrait>> = HashMap::new();
hm.entry(SomeEnum::Type1).and_modify(|b| b.some_other_func()).or_insert(/* HOW */ default());
But I don't really understand the Sized restrictions on trait objects or what Sized is (or why using Default as a Supertrait prevents the object from being Sized). If the entry does not exist in the hashmap, I would like to add the default version of that object to the entry.
Here's an reprex showing my issue.
use std::collections::HashMap;
#[derive(PartialEq, Eq, Hash)]
enum SomeEnum {
Type1,
Type2,
}
// SomeTrait is a subtrait of Default
// Because I want every type to implement Default
trait SomeTrait: Default {
fn new() -> Self {
Self {
..Default::default()
}
}
fn some_other_func(&self);
}
// SomeStruct implements Default and SomeTrait
struct SomeStruct {
foo: i32,
}
impl Default for SomeStruct {
fn default() -> Self {
SomeStruct { foo: 10 }
}
}
impl SomeTrait for SomeStruct {
fn some_other_func(&self) {}
}
fn main() {
let mut hm: HashMap<SomeEnum, Box<dyn SomeTrait>> = HashMap::new();
hm.entry(SomeEnum::Type1)
.and_modify(|b| b.some_other_func())
.or_insert(/* HOW */ default());
}
I cannot use a generic type instead of the trait object because I do/will have multiple implementations of this trait.
I have also tried creating a new trait that is a subtrait of both Default and the one I want:
trait AutoSomeType: SomeType + Default {}
trait SomeType {
fn new() -> Self
where
Self: Sized,
{
Self {
..Default::default()
}
}
fn some_other_func(&self);
}
I got here somehow based on a compiler recommendation, but I feel like this isn't on the right track to a solution.
Looking for "blanket" implementation of the method(s) for trait.
Let's say for a trait
pub trait A {
fn do_a(&self);
}
want to have boxed method that wraps with box, without introducing any additional traits:
fn boxed(self) -> Box<Self>;
I can have another trait to achieve that (playground)
pub trait A {
fn do_a(&self);
}
pub trait Boxed {
fn boxed(self) -> Box<Self>;
}
impl<T> Boxed for T
where
T: A,
{
fn boxed(self) -> Box<Self> {
Box::new(self)
}
}
However, new trait Boxed is required for that.
You can add boxed directly to A with a default implementation so that structs won't need to implement it themselves:
trait A {
fn do_a(&self);
fn boxed (self) -> Box<Self>
where Self: Sized
{
Box::new (self)
}
}
struct Foo{}
impl A for Foo {
fn do_a (&self) {
todo!();
}
// No need to redefine `boxed` here
}
fn main() {
let foo = Foo{};
let _object: Box<dyn A> = foo.boxed();
}
Playground
I have a configuration struct that looks like this:
struct Conf {
list: Vec<String>,
}
The implementation was internally populating the list member, but now I have decided that I want to delegate that task to another object. So I have:
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf<T: Sized + ListBuilder> {
list: Vec<String>,
builder: T,
}
impl<T> Conf<T>
where
T: Sized + ListBuilder,
{
fn init(&mut self) {
self.builder.build(&mut self.list);
}
}
impl<T> Conf<T>
where
T: Sized + ListBuilder,
{
pub fn new(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: lb,
};
c.init();
c
}
}
That seems to work fine, but now everywhere that I use Conf, I have to change it:
fn do_something(c: &Conf) {
// ...
}
becomes
fn do_something<T>(c: &Conf<T>)
where
T: ListBuilder,
{
// ...
}
Since I have many such functions, this conversion is painful, especially since most usages of the Conf class don't care about the ListBuilder - it's an implementation detail. I'm concerned that if I add another generic type to Conf, now I have to go back and add another generic parameter everywhere. Is there any way to avoid this?
I know that I could use a closure instead for the list builder, but I have the added constraint that my Conf struct needs to be Clone, and the actual builder implementation is more complex and has several functions and some state in the builder, which makes a closure approach unwieldy.
While generic types can seem to "infect" the rest of your code, that's exactly why they are beneficial! The compiler knowledge about how big and specifically what type is used allow it to make better optimization decisions.
That being said, it can be annoying! If you have a small number of types that implement your trait, you can also construct an enum of those types and delegate to the child implementations:
enum MyBuilders {
User(FromUser),
File(FromFile),
}
impl ListBuilder for MyBuilders {
fn build(&self, list: &mut Vec<String>) {
use MyBuilders::*;
match self {
User(u) => u.build(list),
File(f) => f.build(list),
}
}
}
// Support code
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct FromUser;
impl ListBuilder for FromUser {
fn build(&self, list: &mut Vec<String>) {}
}
struct FromFile;
impl ListBuilder for FromFile {
fn build(&self, list: &mut Vec<String>) {}
}
Now the concrete type would be Conf<MyBuilders>, which you can use a type alias to hide.
I've used this to good effect when I wanted to be able to inject test implementations into code during testing, but had a fixed set of implementations that were used in the production code.
The enum_dispatch crate helps construct this pattern.
You can use the trait object Box<dyn ListBuilder> to hide the type of the builder. Some of the consequences are dynamic dispatch (calls to the build method will go through a virtual function table), additional memory allocation (boxed trait object), and some restrictions on the trait ListBuilder.
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf {
list: Vec<String>,
builder: Box<dyn ListBuilder>,
}
impl Conf {
fn init(&mut self) {
self.builder.build(&mut self.list);
}
}
impl Conf {
pub fn new<T: ListBuilder + 'static>(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: Box::new(lb),
};
c.init();
c
}
}
I have a configuration struct that looks like this:
struct Conf {
list: Vec<String>,
}
The implementation was internally populating the list member, but now I have decided that I want to delegate that task to another object. So I have:
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf<T: Sized + ListBuilder> {
list: Vec<String>,
builder: T,
}
impl<T> Conf<T>
where
T: Sized + ListBuilder,
{
fn init(&mut self) {
self.builder.build(&mut self.list);
}
}
impl<T> Conf<T>
where
T: Sized + ListBuilder,
{
pub fn new(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: lb,
};
c.init();
c
}
}
That seems to work fine, but now everywhere that I use Conf, I have to change it:
fn do_something(c: &Conf) {
// ...
}
becomes
fn do_something<T>(c: &Conf<T>)
where
T: ListBuilder,
{
// ...
}
Since I have many such functions, this conversion is painful, especially since most usages of the Conf class don't care about the ListBuilder - it's an implementation detail. I'm concerned that if I add another generic type to Conf, now I have to go back and add another generic parameter everywhere. Is there any way to avoid this?
I know that I could use a closure instead for the list builder, but I have the added constraint that my Conf struct needs to be Clone, and the actual builder implementation is more complex and has several functions and some state in the builder, which makes a closure approach unwieldy.
While generic types can seem to "infect" the rest of your code, that's exactly why they are beneficial! The compiler knowledge about how big and specifically what type is used allow it to make better optimization decisions.
That being said, it can be annoying! If you have a small number of types that implement your trait, you can also construct an enum of those types and delegate to the child implementations:
enum MyBuilders {
User(FromUser),
File(FromFile),
}
impl ListBuilder for MyBuilders {
fn build(&self, list: &mut Vec<String>) {
use MyBuilders::*;
match self {
User(u) => u.build(list),
File(f) => f.build(list),
}
}
}
// Support code
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct FromUser;
impl ListBuilder for FromUser {
fn build(&self, list: &mut Vec<String>) {}
}
struct FromFile;
impl ListBuilder for FromFile {
fn build(&self, list: &mut Vec<String>) {}
}
Now the concrete type would be Conf<MyBuilders>, which you can use a type alias to hide.
I've used this to good effect when I wanted to be able to inject test implementations into code during testing, but had a fixed set of implementations that were used in the production code.
The enum_dispatch crate helps construct this pattern.
You can use the trait object Box<dyn ListBuilder> to hide the type of the builder. Some of the consequences are dynamic dispatch (calls to the build method will go through a virtual function table), additional memory allocation (boxed trait object), and some restrictions on the trait ListBuilder.
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf {
list: Vec<String>,
builder: Box<dyn ListBuilder>,
}
impl Conf {
fn init(&mut self) {
self.builder.build(&mut self.list);
}
}
impl Conf {
pub fn new<T: ListBuilder + 'static>(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: Box::new(lb),
};
c.init();
c
}
}