Advice on how to design object constructor

Hi,

I need some advice on how to better design my objects. What I usually have is something like this:

struct Obj {
    var: i32,
}

impl Obj {
    pub fn new(self, x: i32) -> Self {
        Obj { var: x }
    }
}

fn main() {
    let o = Obj::new(6i32);
}


However recently I have been rewriting some c++ libs and many object constructors are heavily overloaded so that object constructors are called in many different ways utilizing the same interface. I Rust that is one big no-no. So my question is how does the community deal with overloading and how would one implement the following while keeping the same interface :

struct Obj {
    var: i32,
}

impl Obj {
    pub fn new(self, x: i32) -> Self {
        Obj { var: x }
    }
    pub fn new(self) -> Self {
        Obj {
            var: rand::random::<i32>(),
        }
    }
}

fn main() {
    let o = Obj::new(6i32); // if provided set the x to 6
    let oo = Onj::new(); // if not provided set it to a rand i32
}

thank you !

If they accept different numbers of arguments, you will have to give them different names. There are some approaches using generics and traits that allow overloading with the same number of parameters, but it's not ideal.

1 Like

Interesting question. Being a newbie I have no idea but from googling around there seems to be some consensus that emulating C++ features in Rust is not the way to go. Specifically method overloading like that is not a desirable way to write code. For example : https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=255025&p=1555757#p1555879

Meanwhile others have used traits to achieve an overloading effect:


Others have done it my passing enumerations as the parameter wich can then carry many different types.

Personally I don't see the need for overloading. Just give your methods different names if they take different parameters. After all, they are doing different things, isn't it an anti-pattern to give different things the same name! This seems to be the approach of wasm-bindgen:
https://rustwasm.github.io/docs/wasm-bindgen/web-sys/function-overloads.html

Here is an excellent article on doing what you might want to do in Rust without C++ style overloading. There are many ways to skin a cat:

The Many Kinds of Code Reuse in Rust http://cglab.ca/~abeinges/blah/rust-reuse-and-recycle/

2 Likes

Thank you !!!

But let me elaborate on my initial post. I am not insisting on overloading but more interested in a convention. So let say that convention we all agree upon states: all objects are called via constructor Obj::new(). An object can be initialized using ones own parameter of generate one by itself. If a user wants to explicitly initialize the object then she/he calls it with a required argument. In the above example, let say a parameter provided to a constructor is a random number. Given there are various ways to make a random (pseudo) number, a user can make one using its own function or can let the constructor to do it for here/him. If a user provides a number then a constructor needs to be able to handle it, if not, then there needs to be a way to let it know that it needs to create one on it own (whatever the underlying algorithm is). Possible solutions to this problem would be:

  1. Provide a "falg" to a constructor to let it know whether it is necessary to generate a number or not:
struct Obj {
    x: i32,
}

impl Obj {
    pub fn new(y: bool) -> Self {
        let a = if y == true {
            rand::random::<i32>()
        }else{
           0
       };
        Obj { x: a }
    }
    pub fn set(&mut self,y: i32)  {
         self.x = y;
    }
    pub fn get(&self) -> i32 {
        self.x
    }
}

fn main() {
    let mut o = Obj::new(true);
   // o.set(6i32); //if the above is set to false
    println!("{}", o.get())
}

(I am not a supporter of this approach as it quickly generates inconsistencies, unnecessary initializations and re-initializations, unnecessary memory allocations and various other issues )

  1. Make separate constructors
struct Obj {
    x: i32,
}

impl Obj {
    pub fn new_make() -> Self {
        Obj {
            x: rand::random::<i32>(),
        }
    }
    pub fn new(y: i32) -> Self {
        Obj { x: y }
    }
    pub fn get(&self) -> i32 {
        self.x
    }
}

fn main() {
    //let o = Obj::new(5i32);
    let o = Obj::new_make();
    println!("{}", o.get())
}

(This would be a Rust way to go but I do not like it. It is way to robust)

  1. implement a function for self-generating the number thus preserving a constructor interface and allowing a user to have the option for initiation and self-inition of a constructor
struct Obj {
    x: i32,
}

impl Obj {
    pub fn new(y: i32) -> Self {
        Obj { x: y }
    }
    pub fn make() -> i32 {
        rand::random::<i32>()
    }
    pub fn get(&self) -> i32 {
        self.x
    }
}

fn main() {
    let o = Obj::new(Obj::make());
   //let o = Obj::new(8i32);
    println!("{}", o.get())
}

(This would be my preference but I am not sure whether it is a good idea or not...)

What else can one do ? In c++ the most elegant solution involves overloading ( prevalent community opinion ), what would be the most elegant solution in Rust (trait overloading is not so elegant if you ask me)?

Thank you

Perhaps you could use better names than new_make. Good names depend on what the object does, but perhaps you could call the random one new and the other one with_id?

2 Likes

Usually you won't encounter times where you'd use overloaded constructors because you'll often accept a generic type, or use different, well-named constructor functions for the different ways of constructing that object.

For example:

struct HoldsAString {
  body: String,
}

impl HoldsAString {
  fn new<S: Into<String>>(body: S) -> Self {
    HoldsAString { body: body.into() }
  }
}

// or

impl HoldsAString {
  /// Construct an object by passing in the field directly.
  fn new(body: String) -> Self {
    HoldsAString { body }
  }

  /// Alternate constructor (or "factory function" if you like) which 
  // will populate the object from a file on disk.
  fn from_file<P: AsRef<Path>>(filename: P) -> Result<Self, io::Error> {
    let body = std::fs::read_to_string(filename)?;
    Ok(HoldsAString::new(body))
  }

  fn random() -> Self {
    let body = create_random_string();
    HoldsAString::new(body)
  }
}
1 Like

First, a comment: typically, you shouldn't take a self parameter in constructor-style functions. If you already need an object in order to create a new object of the same type, how do you solve this chicken-and-egg problem? In Rust, constructor/factory functions are usually methods on the type itself, since there's no need to run code on an uninitialized/invalid instance, as in C++.

To the actual question: if your object optionally needs a large number of parameters to configure, the usual pattern to follow is creating a builder:

#[derive(Debug, Default)]
struct Foo {
    name: String,
    is_baz: bool,
    num_quxes: u32,
}

impl Foo {
    fn name(mut self, name: String) -> Self {
        self.name = name;
        self
    }

    fn baz(mut self, is_baz: bool) -> Self {
        self.is_baz = is_baz;
        self
    }

    fn num_quxes(mut self, num_quxes: u32) -> Self {
        self.num_quxes = num_quxes;
        self
    }
}

fn main() {
    let obj = Foo::default().baz(true).num_quxes(42);
    println!("{:#?}", obj);
}
1 Like

I'd add that good names have meaning, so when given an example like this with the meaning removed from the names (e.g. Obj and y) it becomes very hard to give good advice. I'd call the random initialization something like random rather than new, unless the type is something like a cryptographic key for which randomization is an inherent aspect of creation. For creation from a u32 I'd consider implementing From<u32> if the type can be thought of as a representation of that number, otherwise I'd probably give it a name like with_capacity (or with_id) that reflects what this is being used to create the object.

1 Like

The convention is to have new() and new_with_foo(foo) as separate methods. If there are many more options, then use the builder pattern instead.

2 Likes

To add to what has already been said, check out the standard library collections in this regard. Let's take Vec<T> for example:

There are a couple of well-known "constructors": Vec::new() and Vec::with_capacity(). Then there's also the less-commonly used Vec::from_raw_parts().

Extrapolating from this, I infer the pattern to be:

  1. If your type only needs one constructor, name it new().
  2. If your type needs more than one constructor, name the "base case" trivial constructor new().
  3. If there are one or two other cases to consider, create extra well-named functions to handle these.
  4. For anything more complex, use new() or default() to construct a "base case" and use the builder pattern for maximum flexibility in combinations.

PS Comments on the above summary by more experienced Rustaceans will be much appreciated.

4 Likes

When there is only one optional parameter, I have used an Option as the argument, e.g.,

pub fn new(foo: Option<u8>) -> Foo {
     match foo {
        Some(f) => Foo { count: f },
        None => Foo { count: 0 }
    }
}

I have also used fn new() and fn new_with_foo(foo). I don't have a strong preference.

1 Like

I don't know what the consensus about default parameters are in the Rust ecosystem, but I think your optional parameter can be expressed very well via a default parameter:

class Fraction {
    int m_numerator;
    int m_denominator;

    Fraction(int numerator=0, int denominator=1) :
        m_numerator{ numerator }, m_denominator{ denominator } {}
}

Optional parameters are not a thing in Rust.

1 Like

Can you explain what you mean? I am not sure I understand correctly. Do you mean Rust currently doesn't have default parameters (I know that) or do you mean default parameters are something the Rust community considers bad?

I assume "not a thing" refers to no exact feature which matches default parameters existing in Rust.

There's Option<T> parameters, but yes, the Rust community generally considers using them bad form. It's better to use a builder, or if there are very few of them, separate methods for different types of construction. Writing Obj::new(Some(5)) and Obj::new(None) is rarely more expressive than Obj::with_x(5) and Obj::new().


If your use cases is a constructed / random split like the example, I'd name the constructed one new(x: i32) -> Self, and the random on rand() -> Self. When naming constructors, and methods in general, I try to follow the principle of least surprise. I would personally find it surprising that a new() method with no parameters choose to random-initialize rather than choosing a single sane default.

2 Likes

I wouldn't say the former is any worse, though. It's not more expressive, but not less expressive, either. And if there's a clear dichotomy between the choice of "with this argument" and "without this argument", it's completely justified semantically. It's also easier to implement.

2 Likes