Why would a Trait have a new() method to instantiate Self?

Several times I've seen examples of trait implementations that have a new() method that instantiates the implementing type (Self) and returns that instance.

An example: Traits - Rust By Example

In that example they show an Animal trait being implemented on a Sheep and the Animal trait having a new() method that would create and return a Sheep.

I don't get it. It seems to me that the new() should always been on the Sheep. Maybe it's my OO past but it seems to me that only a Sheep should know how to create itself, I don't understand delegating this to any old trait someone might decided to implement on Sheep

I'm sure I'm missing something painfully obvious, please cue me in!
Michael

The canonical example: Default, which allows you to write one of my favorite functions: std::mem::take

1 Like

Note that having a new(): Self method without arguments is generally considered non-idiomatic - precisely because there's already a trait in standard library (Default) which provides this signature, so one should just implement Default, if this is applicable.

2 Likes

Why i want to have farms

struct Farm<A> {
    animals: Vec<A>
}

each farm start witch 2 Animals "Alice" and "Elvis" and can bread new Animals of the same kind

impl<A: Animal> Farm<A> {
    fn new() -> Self {
        Farm {
            animals: vec![A::new("Alice"), A::new("Elvis")]
        }
    }
    
    fn bread(&mut self) {
        self.animals.push(A::new("Child"))
    }
    
    fn animals(&self) -> &[A] {
        &self.animals
    }
}

but this is only possible if the Animal trait has a method to create new Animals if the new method would be only on the Sheep Farm::new and Farm::bread wouldn't have accees to it.

edit: full example

1 Like

If you want the ability to call new("name") in a generic context, it should be part of the trait. If you don't need that functionality, it wouldn't need to be part of the trait. Often it's not too useful, and in a generic context you might something more like

fn make_more<A: AnimalWithoutNew + Default>(_animal: &A) -> A {
    Default::default()
}

But who knows, maybe you want to be able to

fn spawn_flock<A: Animal>(animal: &A) -> Vec<A> {
    (0..animal.name().len()).map(|_| Animal::new("Nameless")).collect()
}

And more complicated situations exist too.

trait Spawn {
    fn new(alligiance: Alligiance) -> Self;
}

trait MonsterSpawner {
    type Monster: Spawn;
    fn spawning_active(&self) -> bool;
    fn update_spawning_state(&mut self, gs: &GameState);

    fn maybe_generate(&self, alligiance: Alligiance) -> Option<Self::Monster> {
        if self.spawning_active() {
            Some(Self::Monster::new(alligiance))
        } else {
            None
        }
    }
}
1 Like

But it's not any trait. impl Trait for Sheep definitely does belong to the Sheep type, and any trait with such constructor methods should be implemented by the same module in which Sheep is defined. This would in turn make it impossible for anyone else to implement it.

Practically speaking, it would also be impossible for downstream users to implement constructor methods of Sheep directly, since if a type has encapsulated state, then that state should be (and usually is) private.

Untrue. Default requires more type inference. For this reason, many standard container types have inherent nullary new methods, like Vec::new() and HashMap::new(), because they come in handy for specifying concrete types.

3 Likes

Well, it can be used as Type::default anyway, but thanks for correction.

2 Likes

It isn't delegated to the trait, quite the opposite, it's the trait that's delegating to the Sheep. The trait requires that the type provides an implementation, but it doesn't and can't provide one. It works exactly the same as interface methods in OOP, except you're not limited to just methods but you can extend it to associated functions, constants and even types.

6 Likes

There are a few instances where you might want to encode the signature for an object's constructor as part of a trait method, but you are right in that this is often an anti-pattern.

Normally if I want to construct something I'll accept a closure that returns a new instance of that object.

Okay, limiting constructor to same module makes sense. That means if I were the author of that module I'd have to be thinking what's the most natural way for someone to create a Sheep? And the answer is that I'd be offering a constructor on Sheep itself, not on one of its implemented traits.

Another way to look at that is if Sheep implemented several traits, should they all have a new() which creates a Sheep? That seems like a bad idea too. But let's say I did that. Users could create Sheep via various trait names, such as Animal::new(), Delicacy::new(), and so on. But why would they do that? What would it add to their code? They're still ending up with a Sheep. I'm not trying to pick nits here. If the answer is "the example in the book is honestly kind of silly and no one really does that", then I'll forget about it and move on :slight_smile:

First, let's make sure you understand that you can't always choose to just make a Sheep within a generic context.

In a generic context, you don't know the concrete type. You know you're getting something that implements Animal (or Delicacy), but you don't know it's a Sheep, and you can't just call Sheep::new() instead. This won't work:

fn make_friend<A: Animal>(animal: &A) -> A {
    let friend = Sheep::new("friend");
    // The fix:
    // let friend = Animal::new("friend");
    animal.be_friendly(&friend);
    friend
}

It doesn't work because the As must match, and it isn't always Sheep. Perhaps you called fido = make_friend(&dog); for example. There is no subtyping or 'is-a' between the type Sheep and the trait Animal; implementing a trait is a separate concept. Even if there was, though, it would be suprising to find out that fido was a Sheep and not a Dog. (Side note: I'm not going to go into dyn Trait, aka "trait objects". You'll get to them at some later chapter.)

Now, let's return to your question about having multiple ways of doing the same thing via different traits or via the type itself.

The farmer project uses the Animal trait a lot, and depends on creating new Animal-implementing types in a generic manner. They end up creating Sheep (and all other animals) via Animal::new(), and they use the other functionalities of Animal too. Some of these things, not all Declicacy-implementing types can do, or at least we certinaly can't assume that they do.

The chef project uses the Delicacy trait alot, and depends on... [search and replace in the above paragraph].

Within each generic context, the chef and farmer don't know what concrete type they have, and they don't know what other capabilities the types have -- some animals may not be delicacies, and some delicacies may not be animals. They chose their context (Animal, Delicacy -- their trait bounds) based on the capabilities that they needed.

Let's also to tie this to something concrete but less constructed. These do the same thing:

let foo: Vec<usize> = Vec::new();
let bar: Vec<usize> = Default::default();

But in a generic context, you may know that a type implements Default, but not what the type is, or what other traits the type implements. E.g. core::mem::take is defined like so:

pub fn take<T: Default>(dest: &mut T) -> T {
    replace(dest, T::default())
}

It can't rely on Vec::new() or AnyOtherType::new() or even AnyOtherTrait::func(). The trait bound is Default, so that is what it can make use of.

It's really more about "what capabilities does each trait provide" rather than "how do I do X with a particular type". You set up your trait bounds based on what capabilities you need.

...all that being said, like others have mentioned, a constructor being in a trait in particular is somewhat rare. That particular aspect of the example may be "somewhat silly", but don't let that keep you from understanding why you would add methods to a trait even if they overlap with other traits or with the "native" capabilities of the types in question.

3 Likes

No, that's not "another way to look at it", that's a complete mischaracterization of my arguments. I didn't suggest that all traits should declare constructors. I only asserted that it's not a nonsensical, un-idiomatic piece of practice to have some such traits.

If you need construction in a trait so as to use it for abstracting over creation of values in a generic context, you can and should include such a method. If you don't need it, you don't have to. Simple as that.

2 Likes

To be clear, most traits should not have a constructor.

3 Likes

Thanks for your clarity. I wasn't trying to characterize your feedback, I was merely thinking out loud about the possibilities at play, hoping to land on something that made sense in my noggin ..

1 Like

Thankyou for the time you put into that response. I now understand better the idea of dealing with values (only) through their trait's abstractions. As a new learner it's sometimes challenging to understand, and keep in mind, how different parts of the language, and their usage, intersects and influences each other.

1 Like

By the way, if there were several traits that all have a new(), the idiomatic approach would be to have them all inherit from a common base trait that provides the common functionality, in this case new():

trait Constructible {
    fn new(_: &'static str) -> Self;
}
trait Delicacy: Constructible {
   ...
}
trait Animal: Constructible {
   ...
}

That way, Sheep only has to implement new once, for Constructible, and functions over either Delicacy or Animal can access this function using Delicacy::new()/Animal::new() or Constructible::new(); functions working with Sheep can call any of Sheep::new(), Constructible::new(), Delicacy::new(), or Animal::new() and they all refer to the same function, namely Sheep's implementation of the Constructible::new function (which can be written in UFCS style as <Sheep as Constructible>::new() if you want to be really specific about what function you want).

1 Like

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.