Writing code with Rust cost so many lines

Why the code looks so long while writing with Rust? With the same function writing in Kotlin , it seems so easy to read and write. I think that I might be wrong with this program. What's wrong with this??

Rust version

struct Life {
    is_dead: bool,
}
impl Life {
    fn new() -> Life {
        Life {
            is_dead: false
        }
    }
    fn is_alive(&self) -> bool {
        return !self.is_dead;
    }
    fn die_now(&self) -> Life {
        return Life {
            is_dead: true,
        }
    }
}
trait Fittable {
    fn fit(&self);
}
trait Yelled {
    fn yell(&self);
}
struct Cat {
    life: Life,
}
impl Fittable for Cat {
    fn fit(&self) {
        if self.life.is_alive() {
            println!("Fish");
        } else {
            println!("You cannot fit a dead cat");
        }
    }
}
impl Yelled for Cat {
    fn yell(&self) {
        if self.life.is_alive() {
            println!("Meow");
        } else {
            println!("A dead cat cannot yell");
        }
    }
}
impl Cat {
    fn new() -> Cat {
        Cat {
            life: Life::new()
        }
    }
    fn die_now(&self) -> Cat {
        Cat {
            life: self.life.die_now(),
            ..*self
        }
    }
}
struct Dog {
    life: Life,
}
impl Fittable for Dog {
    fn fit(&self) {
        if self.life.is_alive() {
            println!("Bone");
        } else {
            println!("You cannot fit a dead dog");
        }
    }
}
impl Yelled for Dog {
    fn yell(&self) {
        if self.life.is_alive() {
            println!("Woof");
        } else {
            println!("A dead dog cannot yell");
        }
    }
}
impl Dog {
    fn new() -> Dog {
        Dog {
            life: Life::new()
        }
    }
    fn die_now(&self) -> Dog {
        Dog {
            life: self.life.die_now(),
            ..*self
        }
    }
}
fn main() {
    let cat = Cat::new();
    println!("cat is alive? {}", cat.life.is_alive());
    cat.fit();
    cat.yell();
    let cat = cat.die_now();
    println!("cat is alive? {}", cat.life.is_alive());
    cat.fit();
    cat.yell();
    let dog = Dog::new();
    println!("dog is alive? {}", dog.life.is_alive());
    dog.fit();
    dog.yell();
    let dog = dog.die_now();
    println!("dog is alive? {}", dog.life.is_alive());
    dog.fit();
    dog.yell();
}

Kotlin Version:

abstract class Life {
    private var isDead = false;
    fun isAlive(): Boolean {
        return !this.isDead;
    }
    fun dieNow() {
        this.isDead = true;
    }
}
interface Fittable {
    fun fit();
}
interface Yelled {
    fun yell();
}
class Cat: Life, Fittable, Yelled {
    constructor()
    override fun fit() {
        if (this.isAlive()) {
            println("Fish");
        } else {
            println("You cannot fit a dead cat");
        }
    }
    override fun yell() {
        if (this.isAlive()) {
            println("Meow");
        } else {
            println("A dead cat cannot yell");
        }
    }
}
class Dog: Life, Fittable, Yelled {
    constructor()
    override fun fit() {
        if (this.isAlive()) {
            println("Bone");
        } else {
            println("You cannot fit a dead dog");
        }
    }
    override fun yell() {
        if (this.isAlive()) {
            println("Woof");
        } else {
            println("A dead dog cannot yell");
        }
    }
}
fun main() {
    val cat = Cat();
    println("cat is alive? ${cat.isAlive()}");
    cat.fit();
    cat.yell();
    cat.dieNow();
    println("cat is alive? ${cat.isAlive()}");
    cat.fit();
    cat.yell();
    val dog = Dog();
    println("dog is alive? ${dog.isAlive()}");
    dog.fit();
    dog.yell();
    dog.dieNow();
    println("dog is alive? ${dog.isAlive()}");
    dog.fit();
    dog.yell();
}
1 Like

Hi and welcome to this forum. This is the “internals” forum where we discuss changes to the design of the Rust language and standard library, and sometimes also talk about implementation details of the rustc compiler, or other things related to contributing to the development of rustc.

Your questions seems more about trying to get a better understanding of the language Rust, and I guess you’re also asking how your “program” (though it’s arguably more of an object-oriented-language toy-example demonstration, not exactly a realistic “program”) could be written more concisely and/or more ideomatically in Rust.

For questions like these, I suggest you move over to the “users” forum at https://users.rust-lang.org/. That’s the more appropriate place for your question and you can expect more and better answers over there.

3 Likes

In any case, let me give you a quick answer here.

Rust doesn’t fully support object-oriented programming. In particular, we value separation of data and behavior. We don’t have the kind of inheritance that (abstract) classes in Java offer, where you inherit both fields and methods together.

In situations where this form of inheritance works, you can often achieve the same thing with delegation. In fact, your Rust code does do some form of delegation, unfortunately this comes with boilerplate code, as you noticed.

One solution to boilerplate code in Rust is to use macros. Before we go there, in a first step to coming closer to your Kotlin code, we should change die_now from fn die_now(&self) -> Self to fn die_now(&mut self) -> (). (And also let’s remove some more unnecessary return keywords.)

struct Life {
    is_dead: bool,
}
impl Life {
    fn new() -> Life {
        Life { is_dead: false }
    }
    fn is_alive(&self) -> bool {
        !self.is_dead
    }
    fn die_now(&mut self) {
        self.is_dead = true
    }
}
trait Fittable {
    fn fit(&self);
}
trait Yelled {
    fn yell(&self);
}
struct Cat {
    life: Life,
}
impl Fittable for Cat {
    fn fit(&self) {
        if self.life.is_alive() {
            println!("Fish");
        } else {
            println!("You cannot fit a dead cat");
        }
    }
}
impl Yelled for Cat {
    fn yell(&self) {
        if self.life.is_alive() {
            println!("Meow");
        } else {
            println!("A dead cat cannot yell");
        }
    }
}
impl Cat {
    fn new() -> Cat {
        Cat { life: Life::new() }
    }
    fn die_now(&mut self) {
        self.life.die_now()
    }
}
struct Dog {
    life: Life,
}
impl Fittable for Dog {
    fn fit(&self) {
        if self.life.is_alive() {
            println!("Bone");
        } else {
            println!("You cannot fit a dead dog");
        }
    }
}
impl Yelled for Dog {
    fn yell(&self) {
        if self.life.is_alive() {
            println!("Woof");
        } else {
            println!("A dead dog cannot yell");
        }
    }
}
impl Dog {
    fn new() -> Dog {
        Dog { life: Life::new() }
    }
    fn die_now(&mut self) {
        self.life.die_now()
    }
}
fn main() {
    let mut cat = Cat::new();
    println!("cat is alive? {}", cat.life.is_alive());
    cat.fit();
    cat.yell();
    cat.die_now();
    println!("cat is alive? {}", cat.life.is_alive());
    cat.fit();
    cat.yell();
    let mut dog = Dog::new();
    println!("dog is alive? {}", dog.life.is_alive());
    dog.fit();
    dog.yell();
    dog.die_now();
    println!("dog is alive? {}", dog.life.is_alive());
    dog.fit();
    dog.yell();
}

Now, let’s use macros. There’s a few crates out there that can help with macros to reduce boilerplate. Let’s start with new methods. While in this case, you could technically probably just use ::default instead of ::new and derive the Default implementations, it’s good style to still also have a new method. Let’s use the derive-new crate to help out.

use derive_new::new;

#[derive(new)]
struct Life {
    #[new(value = "false")]
    is_dead: bool,
}
impl Default for Life {
    fn default() -> Self {
        Self::new()
    }
}
impl Life {
    fn is_alive(&self) -> bool {
        !self.is_dead
    }
    fn die_now(&mut self) {
        self.is_dead = true
    }
}
trait Fittable {
    fn fit(&self);
}
trait Yelled {
    fn yell(&self);
}

#[derive(new, Default)]
struct Cat {
    #[new(default)]
    life: Life,
}
impl Fittable for Cat {
    fn fit(&self) {
        if self.life.is_alive() {
            println!("Fish");
        } else {
            println!("You cannot fit a dead cat");
        }
    }
}
impl Yelled for Cat {
    fn yell(&self) {
        if self.life.is_alive() {
            println!("Meow");
        } else {
            println!("A dead cat cannot yell");
        }
    }
}
impl Cat {
    fn die_now(&mut self) {
        self.life.die_now()
    }
}

#[derive(new, Default)]
struct Dog {
    #[new(default)]
    life: Life,
}
impl Fittable for Dog {
    fn fit(&self) {
        if self.life.is_alive() {
            println!("Bone");
        } else {
            println!("You cannot fit a dead dog");
        }
    }
}
impl Yelled for Dog {
    fn yell(&self) {
        if self.life.is_alive() {
            println!("Woof");
        } else {
            println!("A dead dog cannot yell");
        }
    }
}
impl Dog {
    fn die_now(&mut self) {
        self.life.die_now()
    }
}
fn main() {
    let mut cat = Cat::new();
    println!("cat is alive? {}", cat.life.is_alive());
    cat.fit();
    cat.yell();
    cat.die_now();
    println!("cat is alive? {}", cat.life.is_alive());
    cat.fit();
    cat.yell();
    let mut dog = Dog::new();
    println!("dog is alive? {}", dog.life.is_alive());
    dog.fit();
    dog.yell();
    dog.die_now();
    println!("dog is alive? {}", dog.life.is_alive());
    dog.fit();
    dog.yell();
}

Now, onto the die_now method. You’re delegating manually here, also you’ve actually skipped the opportunity to add the is_alive method to Cat/Dog. To begin with out Rust solution, let’s separate behavior from data first, but turning Life into a trait and a struct:

struct LifeStruct {
    #[new(value = "false")]
    is_dead: bool,
}
trait Life {
    fn is_alive(&self) -> bool;
    fn die_now(&mut self);
}

and implement it

impl Life for LifeStruct {
    fn is_alive(&self) -> bool {
        !self.is_dead
    }
    fn die_now(&mut self) {
        self.is_dead = true;
    }
}

Now we can use a crate that spares us boilerplate for delegation, e.g. ambassador, in order to create trait implementations of Life for Cat and Dog as-well.

All we need to do is add an annotation on the trait

#[delegatable_trait]
trait Life {
    fn is_alive(&self) -> bool;
    fn die_now(&mut self);
}

and a derive and annotation on the structs, e.g.

#[derive(new, Default, Delegate)]
#[delegate(Life)]
struct Cat {
    #[new(default)]
    life: LifeStruct,
}

The Cargo.toml file now contains a section

[dependencies]
ambassador = "0.2.1"
derive-new = "0.5.9"

and the full code is

use ambassador::{delegatable_trait, Delegate};
use derive_new::new;

#[derive(new)]
struct LifeStruct {
    #[new(value = "false")]
    is_dead: bool,
}
impl Default for LifeStruct {
    fn default() -> Self {
        Self::new()
    }
}

#[delegatable_trait]
trait Life {
    fn is_alive(&self) -> bool;
    fn die_now(&mut self);
}
impl Life for LifeStruct {
    fn is_alive(&self) -> bool {
        !self.is_dead
    }
    fn die_now(&mut self) {
        self.is_dead = true;
    }
}

trait Fittable {
    fn fit(&self);
}
trait Yelled {
    fn yell(&self);
}

#[derive(new, Default, Delegate)]
#[delegate(Life)]
struct Cat {
    #[new(default)]
    life: LifeStruct,
}
impl Fittable for Cat {
    fn fit(&self) {
        if self.is_alive() {
            println!("Fish");
        } else {
            println!("You cannot fit a dead cat");
        }
    }
}
impl Yelled for Cat {
    fn yell(&self) {
        if self.is_alive() {
            println!("Meow");
        } else {
            println!("A dead cat cannot yell");
        }
    }
}

#[derive(new, Default, Delegate)]
#[delegate(Life)]
struct Dog {
    #[new(default)]
    life: LifeStruct,
}
impl Fittable for Dog {
    fn fit(&self) {
        if self.is_alive() {
            println!("Bone");
        } else {
            println!("You cannot fit a dead dog");
        }
    }
}
impl Yelled for Dog {
    fn yell(&self) {
        if self.is_alive() {
            println!("Woof");
        } else {
            println!("A dead dog cannot yell");
        }
    }
}

fn main() {
    let mut cat = Cat::new();
    println!("cat is alive? {}", cat.is_alive());
    cat.fit();
    cat.yell();
    cat.die_now();
    println!("cat is alive? {}", cat.is_alive());
    cat.fit();
    cat.yell();
    let mut dog = Dog::new();
    println!("dog is alive? {}", dog.is_alive());
    dog.fit();
    dog.yell();
    dog.die_now();
    println!("dog is alive? {}", dog.is_alive());
    dog.fit();
    dog.yell();
}
6 Likes

My 2 cents (I'm just a user)

  1. This belongs to https://users.rust-lang.org/
  2. Rust is usually way more lines than many other languages. I also use Python and Julia and those are way shorter. Rust has many strengths, but shortness is not one of them, it was not a design goal and many tradeoffs were made that made rust code longer, but better in a different way instead. That being said, I disagree with the thesis that "number of lines" equals "time to write". The biggest factor in how much time is needed to write an application is not how fast you can type, but how easy it is to debug and read. And rust is way easier to debug, many problems that take me half an hour in C++ or julia to debug are a simple compile error in rust that can be found and fixed in a minute. Rustc's error messages are really nice. Rust code is also easier to read, because of rust-analyzer. In Vscode, (if the correct extensions are installed and configured) you can put your cursor on a function call, press F12 (a.k.a Go To Definition) and the cursor will jump to the function definition. Similar features exists for other languages, but most of them are way less reliable than rust-analyzer.

About your example:

  1. Always run cargo clippy and fix the warnings
  2. As clippy suggests, the return keyword and the semicolon can be emitted, expr is equivalent to return expr;
  3. As steffahn has noted, some code can be shortened (and made more similar than the Kotlin variant) by mutating an Object instead of constructing a new one:
    impl Life {
    ...
        fn die_now(&mut self) {
            self.is_dead = true;
        }
    }
    impl Cat {
        ...
        fn die_now(&mut self) {
            self.life.die_now();
        }
    }
    fn main() {
        let mut cat = Cat::new();
        println!("cat is alive? {}", cat.life.is_alive());
        cat.fit();
        cat.yell();
        cat.die_now();
        println!("cat is alive? {}", cat.life.is_alive());
        cat.fit();
        cat.yell();
    }
  1. This won't make it shorter in your case, but Self is a keyword that is the type of the current struct. So, you could also write
    fn die_now(&self) -> Self {
        return Self { is_dead: true };
    }

Now let's look at some small differences between the your rust and your Kotlin code:

  1. In Rust, but not in Kotlin you have to write
    fn new() -> Cat {
        Cat { life: Life::new() }
    }

maybe there is some macro to #[derive(..)] this. 2. In Rust, but not in Kotlin, you have to write impl Life. This is longer, but has the advantage that you can define methods at multiple different places, as long as it's in the same crate (doesn't have to be in the same module or file. Maybe there is some macro-syntax-sugar for this. My advice: Don't use macro-syntax-sugar it's a recipe for desaster. 3. I like the syntax of println("cat is alive? ${cat.isAlive()}"). I whish rust had that. 4. In the Kotlin code it is less clearly to see that override fun fit belongs to the Fittable interface and not to the Yelled interface.

There is another difference. One that isn't some superficial syntax-difference, but an important, deeply engrained language design difference. In Kotlin (and e.g. C++) you can inherit methods. Like this

abstract class Life {
    ...
    fun dieNow() {
        this.isDead = true;
    }
}
class Cat: Life {
    ...
}

Now you can call cat.dieNow(), where as in Rust, you had to add

    fn die_now(&mut self) {
        self.life.die_now();
    }

Discussion the advantages and disadvantages of this (I think it's called method inheritance) is too big of a topic for a forum comment. (One disadvantage of method inheritance is that it often breaks the Go To Definition feature of many IDE's.) Maybe someone can link a blog post or talk or paper about this. One disadvantage of Rust here, is that if life had n methods and You had m animals, you would need n*m of those do-nothing-wrappers. This is difficult to avoid. Possible ways are

  1. As Steffahn has noted, Macros. They can make your code shorter, but they can also make you code horrible unreadable. Steffahn pointed out the existance of ambassador.
  2. As Steffahn has noted, Replace cat.die_now() with cat.life.die_now().
  3. This way you only need n+m instead of n*m do-nothing-wrappers.
trait Animal {
    fn get_life(&mut self) -> &mut Life;
}
impl Animal for Cat {
    fn get_life(&mut self) -> &mut Life {
        &mut self.life
    }
}
impl Animal for Dog {
    fn get_life(&mut self) -> &mut Life {
        &mut self.life
    }
}
fn die_now<T: Animal>(x: &mut T) {
    x.get_life().die_now();
}

Also, while traits are rusts equivalent of inheritance or interfaces in other languages, you should use traits in Rust less often than inheritance and interface in other languages. You should try to write different style of code in different languages. For example, I wrote a lot of code that looks like this:

struct Animal {
    species: Species,
    // properties that all animals have go here
    is_dead: bool,
    is_male: bool,
    age: i32,
}
enum Species {
    Bird(Bird),
    Dog(Dog),
}
struct Bird {
    // properties that only birds have go here
    wingspan: f64,
}
struct Dog {
    // properties that only dogs have go here
    name: String,
}
impl Animal {
    fn is_alive(&self) -> bool {
        !self.is_dead
    }
    fn yell(&self) {
        if self.is_alive() {
            match self.species {
                Species::Bird(_) => println!("*bird noises*"),
                Species::Dog(ref dog) => println!("woof, my name is {}", dog.name),
            }
        } else {
            println!("Dead animals are silent");
        }
    }
    fn die_now(&mut self) {
        self.is_dead = true;
    }
}
fn main() {
    let mut dog = Animal {
        species: Species::Dog(Dog { name: "rex".into() }),
        is_dead: false,
        is_male: true,
        age: 5,
    };
    println!("dog is alive? {}", dog.is_alive());
    dog.yell();
    dog.die_now();
    dog.yell();
}

This has many advantages and disadvantages. One disadvantage is that you have to list all Animals in one enum. You cannot define a third animal type in a different crate.

(I have no idea why I took the time to write this, I need to stop wasting my time writing stuff like this in forums and get back to work.)

1 Like

Thanks for pointing out ambassador's existance. I did not know that.

Feel free to discuss this further on the Users forum.

1 Like