How to program shared behaviors in Rust without repeating same code in each module?
Asked Answered
S

1

4

For writing a very large program, I see no way to alleviate having to write the same code for each struct that uses a certain shared behaviour.

For example, Dog may "bark":

struct Dog {
    is_barking: bool,
    ....
}
impl Dog {
    pub fn bark(self) {
        self.is_barking = true;
        emit_sound("b");
        emit_sound("a");
        emit_sound("r");
        emit_sound("k");
        self.is_barking = false;
    }
    ....
}

And many breeds of this dog may exist:

struct Poodle {
    unique_poodle_val: &str
}
impl Poodle {
    pub fn unique_behaviour(self) {
        self.some_behaviour();
    }
}

struct Rottweiler {
    unique_rottweiler_val: u32
}
impl Rottweiler{
    pub fn unique_behaviour(self) {
        self.some_behaviour();
    }
}

The issue is that Rust seems incapable of this in my current knowledge, but it needs to be done and I need a workaround for:

  1. Allowing Poodle and Rottweiler to bark using the exact same behavior which the breeds should not need to regard.
  2. Allowing this to be possible without recoding bark() in every breed module, which is programming hell as it leads to repetitious code and every module has to implement bark().
  3. Traits are the inverse and cannot access the struct, so default-trait implements do not work. Rust does not support OOP-like inheritance and nor is it desired here.

Therefore, I ask: How would it be possible to implement bark() without rewriting bark() in each module, since Poodle and Rottweiler bark exactly the same way after all?

Please provide an example of how to solve this issue in Rust code, idiomatic and slightly hacky solutions are welcome but please state which they are as I am still learning Rust. Thank you.

Edit: The boolean is not a thread thing, rather it's a example of setting some state before doing something, i.e. emit_sound is within this state. Any code we put into bark() has the same issue. It's the access to the struct variables that is impossible with say, traits.

Sixtyfourmo answered 12/8, 2021 at 11:9 Comment(16)
You can't have it totally shared but you may either isolate the barking impl in a struct that all dogs would own, or define a trait requiring that implementers ensure getting/changing is_barking. If it matters enough, you may create a derive macro to make it less verbose (but I wouldn't recommend it as ad-hoc proc macros are painful to manage today).Inotropic
The simple fact that you have the Dog, Poddle and Rottweiler structs means you made an initial design error, though: this is an OOP thinking, while Rust isn't based on OOP principles. You should start the design from the desired features rather than from an object view. If we start from your structs and add the Barking property, it soon starts to look as nothing which would exist in a real Rust code.Inotropic
Finally note that there's no reason to have a bool set to a value during the execution of a function. The ownership's model of Rust makes it nonsensical.Inotropic
As I see from that, it makes it impossible to downcast the struct, since there can never be, how would be a best practice to give awareness of the child struct to the parent? I know this it's possible in generics in C++ for example, but Rust is very against circular reference, no? I presume the parent could hold Parent<Child> but then we wouldn't be able to send the Child at the constructor stage. It should be logically possible because the two structs are then separate, i.e. Parent and Child are no longer the same reference but they own I presume Ref/Cell-like "Option"al refs to each other.Sixtyfourmo
The "bool" case is only a demonstration, I am fully aware of the issue of the execution, but the logic itself is perfectly sound. Even if we were to use a Mutex of sorts to change some data. This is a single-threaded case. There are no threads in WASM and the memory of the VM is essentially static.Sixtyfourmo
There's no "parent" here. There's a barking behavior which all dogs have. I repeat: Rust isn't OOP and you should avoid thinking in term of inheritance.Inotropic
I don't get how you meant to relate your Dog and Poodle/Rottweiler. Do you want each breed to contain a Dog? Do you want Dog to have a breed that is a tagged union? Something else?Cuthbert
We cannot fix the solution without knowing the problem. This code doesn't solve a problem, it's a toy example that in some other language would demonstrate inheritance. But Rust doesn't have inheritance, so it's a fish out of water; it's useless. When designing programs you must start with requirements and constraints: not half-baked ideas that work in some other language. You can't translate structural designs from other languages. It does not work. Describe the actual problem you are trying to solve.Ordzhonikidze
"Do you want each breed to contain a Dog?" I want it to be logical and real-world as possible, I want the breeds to have barking but in Rust either the breed or the dog is the struct, I can't have both according to that. Dog are instances of a breed in real-life, is rust incapable of real-life logic or is there a better solution? My actual issue is giving dog breeds barking without repeating code. So the code is not a toy-example. I would like to see how this would be done. I want code with barking shared. Putting the dog in the breed doesn't make logical sense or me to upcast Breed to Dog.Sixtyfourmo
Re: "My actual issue is giving dog breeds barking without repeating code" That is not your problem, that's the solution which you have imagined must exist. But it doesn't. You've already trapped yourself into what is most likely a bad design by making assumptions like "Poodle must be a struct" and "bark must be a method". Again, requirements and constraints: what does this program do besides demonstrate a bad design?Ordzhonikidze
In asking "what does this program do?" I'm hoping to get an answer like "this is my new game, Kennel Simulator 2022" or "I'm creating an extremely elaborate model railroad system" or "I'm running simulations to evaluate what breed of dog would be best at rescuing people" or some other context in which Dog and bark make sense as program elements, not just as placeholders.Ordzhonikidze
Are you saying poodles don't bark, trent, because they certainly do. Nature is not badly designed, hierarchy is natural. And the issue is repetition, how does one make several breeds able to bark without coding the exact same thing for every breed? Because they certainly don't need to bark any differently. If the argument is that such code causes a crash in other languages (which I've never seen it do so) then surely Rust is worse in this sense because it's essentially logically crashed before you even implement it. It's perfectly valid for dogs to bark, now as for without repetition in Rust?Sixtyfourmo
"Nature" is not a Rust program so I don't really get what your point is here. What program are you making? It is a simple question. You can't design a program without knowing what it should do. Start with requirements and constraints. You can't start from a broken program and fix it unless you know what it's supposed to do.Ordzhonikidze
Well it is a nature simulator, how would one do that in Rust, @trentcl I would show a UML diagram but up to now it seems like Rust cannot work with UML as it's not unified (anti-component but with composition), how would one make a Rust diagram or design ahead to find a suitable paradigm? I don't see any standard nor docs on this. But it is not so much the paradigm it is the repetition of code, if several things share behaviour, Rust requires all to implement this behavior is what's been said so far with only the makeshift of putting parent into child which is anti-syntactical inheritanceSixtyfourmo
Even in nature itself, the "inheritance" analogy is flawed. Fifi doesn't bark the way she does because she is a Poodle, but because she has a VoiceBox which is of a particular shape and size and also has a BarkingBehavior that she learned from other dogs. Poodle is an idea purely in the minds of some humans who find it easier to understand nature by pretending that sharp dividing lines exist where, in reality, none do.Ordzhonikidze
Fifi the Poodle doesn't make themselves. they inherit genes from a copy which they duplicate and mutate, they do not multiply their own source code infinitely. And certainly no programmer got time to do that. If every cellular being had to reimplement their own genetic code with a highly conscious effort to match features., then biology would be even more needing of sound logic anyway to make it efficient, but either way it's not. It's bad responsibilities, the poodle is mostly copied, mutations are minor not compositional.Sixtyfourmo
V
8

You've put the finger on something that Rust doesn't do nicely today: concisely add to a struct a behavior based on both internal data and a function.

The most typical way of solving it would probably be to isolate Barking in a struct owned by all dogs (when in doubt, prefer composition over inheritance):

pub struct Barking {
    is_barking: bool,
}
impl Barking {
    pub fn do_it(&mut self) {
        self.is_barking = true; // <- this makes no sense in rust, due to the ownership model
        println!("bark");
        println!("a");
        println!("r");
        println!("k");
        self.is_barking = false;
    }
}

struct Poodle {
    unique_poodle_val: String,
    barking: Barking,
}
impl Poodle {
    pub fn unique_behaviour(self) {
        println!("some_behaviour");
    }
    pub fn bark(&mut self) {
        self.barking.do_it();
    }
}

struct Rottweiler {
    unique_rottweiler_val: u32,
    barking: Barking,
}
impl Rottweiler{
    pub fn unique_behaviour(self) {
        println!("unique behavior");
    }
    pub fn bark(&mut self) {
        // maybe decide to bite instead
        self.barking.do_it();
    }
}

In some cases it can make sense to define a Barking trait, with a common implementation and declaring some functions to deal with the state:

pub trait Barking {
    fn bark(&mut self) {
        self.set_barking(true);
        println!("bark");
        println!("a");
        println!("r");
        println!("k");
        self.set_barking(false);
    }
    fn set_barking(&mut self, b: bool);
}

struct Poodle {
    unique_poodle_val: String,
    is_barking: bool,
}
impl Poodle {
    pub fn unique_behaviour(self) {
        println!("some_behaviour");
    }
}
impl Barking for Poodle {
    fn set_barking(&mut self, b: bool) {
        self.is_barking = b;
    }
}

Beware that this half-OOP approach often ends up too much complex and less maintainable (like inheritance in OOP languages).

Vibrant answered 12/8, 2021 at 11:57 Comment(11)
To me, this seems far more OOP than inheritance. Surely, the Barking should be able to be stateful in real world logic, this creates surplus cases of functionality. Whereas in a multi-parental inheritance hierarchy the behaviour is optional without requiring the composition. Due to that, you can never even ask if the dog breed can bark without repeating code in each Dog that determines whether Barking or can-bark is implemented. I'm not sure how to create that safety in Rust without repeating the code. Option exists but the field is not foreknown.Sixtyfourmo
Your second example leads to "impl Barking for BreedHere {" needing to be written repetitive in every module, so the first example is better than the second. But the fact that the Poodle is a Dog is lost, and there is no way to use it as Dog impartially, nor know if Poodle or Rottweiler is a dog without coding in every possible breed (again repetitious evil).Sixtyfourmo
"so the first example is better than the second". It depends. I've used both in real applications. The second one globally brings the same feeling, benefits and problems than inheritance in practice (you'd think about the trait as an abstract class in this case).Inotropic
I've never seen issues with inheritance in my 30 years as a software developer, but I've seen plenty with this composition process e.g. repetitious code, functional came before OOP and was made to solve those issues Rust seems to have regressed to. Criticism is valuable only if you have an alternative. Maintaining repetitious code is more prone to errors. It would seem the only way to achieve logistics without repetitious pollution is your first problem but it lacks the castability. Is there no solution in regards of coercing a child struct in a pointerly fashion? Or a crate, at least?Sixtyfourmo
You may have noticed all recent languages departed from OOP. With a reason. But comments aren't the place to compare paradigms or personal experiences. Of course Rust has patterns to write programs, with strengths and weaknesses. There have been other comments pointing out the need to start from a real problem and not a textbook inheritance example if you want a real app design. I've provided the tracks I find the most useful with what I understood of your question.Inotropic
I can't say I've noticed, but there are some languages that are for a specific purpose that don't need OOP, especially scripted ones. TS, ES6-JS, Kotlin, Swift, Py3, all moved towards OOP not away from it. Universities teach it, jobs are wanting it. I would even say Rust has elements of OOP and is ruled heavily by "subject.verb(message") structure just lacks in capability which is echoes of the failures in Go. That baseless dogma should not impact on logic, since it does not help. Good production values practicality, I have yet to see a practical example of a better non-repetitious alternativeSixtyfourmo
@YaroslavaLaurie it's hard or impossible to argue if you don't ask a real concrete thing you want to do. You claim 30 years experience but I don't think you have more than 2 days experience in Rust, still you claim "functional came before OOP and was made to solve those issues Rust seems to have regressed to". Maybe true maybe false I can't say i don't fully understand this level of english but what I can say for sure is that you can't know this yet. Try Rust for more time, rust is NOT EASY to understand. play.rust-lang.org/?gist=b451c98d80fa1496b26adc1a15aa6642.Appurtenance
I have 5 months on and off experience in Rust. Yes functional programming is the result of the maneuvre away from procedural programming with the avoidance of GOTO commands, instead the compiler would decide the jump logic based on what was termed symbolic signatures. Your provided coded is not capable of accessing data in Dog or Cat, which is composition without compositional members. How would you access shared variables, this feature seems useless since the bark() is external and not behavioural of the object, so how are Rust traits at all traits and not just to-implement interface headers?Sixtyfourmo
@YaroslavaLaurie it's not useless one could use this to compile time check that something can Barking play.rust-lang.org/…. Again if you don't ask me a practical thing I can't answer you. There is a LOT of tool in Rust and I can't randomly guess the one you want. "How would you access shared variables" this is an anti pattern in Rust, we don't do that.Appurtenance
What if the bark() sets aspects of the Dog, since we can't access aspects that make it a Dog, and the trait Barking can't set it, nor set anything within the scope of Barking, how would the dog say invoke has_barked = true, or the motion of opening the mouth. There's many internal aspects on a dog that a bark would involve. An anti-pattern would advise better else there is not better, e.g. mortality can be regarded an anti-pattern in life, but there's no solution for it and I need a real world solution with the code here.Sixtyfourmo
The linked example is blank, is your link wrong? I/we are trying to learn Rust, but there's no real literature on these paradigms. Logic should be defined by logician, it's not a programmer's role to define the logic of a stateful system. However the goal is not the paradigm but a method to have no repetitious code when giving the same thing the same behaviour, in animal organs interact, so Barking is external and lacks entanglement of systematic variables that allow mechanical changes e.g. inner state mutation for a species. Also, macros here are a hack and only in a sense rewrite Rust.Sixtyfourmo

© 2022 - 2024 — McMap. All rights reserved.