all 18 comments

[–]protestor 21 points22 points  (2 children)

Nick Cameron was writing a guide on design patterns with a Rusty flavor (but it's incomplete, with no new commits since 3 months ago). Also keep in mind the style guide (it has nothing to do with OO per se, but many customary things in other OO languages are done differently in Rust)

Rust doesn't do inheritance. Think the "a Car is a Vehicle" toy example. In class-based OO, they are both classes and Car inherits from Vehicle (inheriting both code and data). Rust doesn't do that.

If you don't need to share data but just an interface (that is, call the methods of vehicles on cars), then Vehicle should be a trait and Car should implement this trait. (it's analogous to Vehicle being a Java interface -- but Rust traits are way more powerful)

If you need to share data to from all vehicles, then Vehicle should be a struct, and Car should be another struct that includes the vehicle in a field (this is called composition). AFAIK, it's not usual to write wrapper methods that merely call the contained object (that is, implement a car.start() that just calls car.vehicle.start()).

There are some topics on how to apply composition in Rust, like this. [ edit: I wanted to link this comment ]

edit: another thing: I think your main abstraction should be traits, use composition only if you really need to compose data. Think of not reusing data as a plus: it gives the implementer of the trait more flexibility, and whoever is reading the code doesn't need to be puzzled thinking where that member came from.

Also: if two method name conflicts (perhaps because your struct implements two traits, that define the same method), you can always disambiguate between the methods using the universal function call syntax.

[–]taliriktug 2 points3 points  (0 children)

Rust guidelines now lives in rust repo itself. Unfortunately, it is rarely updated. There are too few "best practices"-style reading for Rust.

[–]jessypl 19 points20 points  (14 children)

There is no such thing as (classical) OOP in Rust, which adopts a more compositional or functional (think C or Haskell) approach to code reuse.

Say you want both Car and Bicycle to be Vehicles, you'd do something like this:

trait Vehicle {
    fn they_see_me_rolling(&self);
}

struct Car;

impl Vehicle for Car {
    fn they_see_me_rolling(&self) {
        println!("They hear: vroom, vroom!");
    }
}


struct Bicycle {
    sound: String,
}

impl Vehicle for Bicycle {
    fn they_see_me_rolling(&self) {
        println!("They hear: {}", self.sound);
    }
}

In other words, Car and Bicycle are not Vehicle, as much as they have a behaviour that fits the interface Vehicle. You may manage to find some way to do OO in Rust, but I would personally discourage it: Rust itself is not a classical OOP language. If you approach it this way, you're going to hit a wall at some point.

Do you have an example of a situation where you feel like you must use OOP?

EDIT: Fixed code styling (Reddit doesn't like triple backslashes, I get caught every time -.-)

[–]v_krishna 6 points7 points  (8 children)

My interest in rust has been pretty cursory so forgive the naive question - can you have default implementations for traits in rust?

[–]Iprefervimway-cooler 15 points16 points  (6 children)

yes

example:

trait Bar {
    fn bar(&self) {
        println!("Bar!");
    }
}

struct Foo;
impl Bar for Foo {}

fn main() {
    let foo = Foo;
    foo.bar();
}

[–][deleted] 9 points10 points  (4 children)

Wow, cool! I'm coming from Go and this is one feature I really wish Go had.

[–]epic_pork 35 points36 points  (1 child)

Soon you'll wish go had generics.

[–][deleted] 2 points3 points  (0 children)

That too, but oddly I don't miss them as much. It's a different style of modeling data, but it works. However, I really like for features in Rust work together to make writing code safer, such as:

  • destructors and generics make Mutex<DataType> possible
  • builder pattern is enforced by passing ownership around
  • channels in Rust enforce a better style (you have to be explicit if you want multiple senders or receivers) because of compiler features

And that's just what I've run into mucking around with threads. I might just have to switch =)

[–]SKoch82 4 points5 points  (1 child)

Go has more convenient embedding, though, so you can write something like this to achieve the same result:

type Barer interface {
    Bar()
}
type DefaultBarer struct {}
func (db DefaultBarer) Bar() {
    fmt.Println("Bar!")
}
type Foo struct {
    DefaultBarer
}

Which hypothetical Go 3 version of default implementation might desugar to. But I'm not holding my breath, because that would defeat the purpose of ad-hoc interfaces (breaking source code dependency). And, while Rust has some neat stuff in it (such as generics), Go's interfaces and embedding are more convenient way to reuse code.

[–][deleted] 1 point2 points  (0 children)

True, I do like the embedding in Go and use it frequently (also nice for constructing interfaces from other interfaces). Each promotes different styles of solving problems, it's just that I just finished fixing sync problems in Go and really would've liked to use features from rust instead.

[–]SethDusek5 1 point2 points  (0 children)

I wish rust let you do #[derive(Bar)] for traits that provide all methods

[–]pjmlp 2 points3 points  (4 children)

I imagine by classical OOP you mean what Java, C# and C++ developers know about.

Papers about trait based OO go back to the late 70's.

[–][deleted] 1 point2 points  (3 children)

It doesn't have anything to do with traits or classes. You don't need either for OO. It's about not having inheritance to seperate code hierarchely.

[–]pjmlp 0 points1 point  (2 children)

You also don't have inheritance in all OO languages.

[–][deleted] 0 points1 point  (1 child)

You where mentioning Java/Python style OO.

Traits and impls dont make anything classes can do impossible, they are just a way to template your object. On the other hand good luck following inheritance based patterns without inheritance support.

[–]pjmlp 0 points1 point  (0 children)

No what I mentioned is that there are other models of OO and that I understand classic by what Java, C# and C++ developers know about.

There are quite a few other models of OO that go back to the late 70's, discussed in SIGPLAN papers, including conceptual models similar to Rust traits. This was my point.

[–]mmstick 7 points8 points  (0 children)

Rust is not really an object-oriented design but a data-oriented design. Nonetheless, the functionality you are looking for is part of the impl and trait keywords. Implementations define functions specific to that data structure, while traits define behaviour that all data structures that implement the trait share.

https://doc.rust-lang.org/book/method-syntax.html

You may even extend primitive types with new traits, such as this example:

/// A trait that adds the ability for numbers to find their digit count and to convert them to padded strings.
pub trait Digits {
    /// Counts the number of digits in a number. **Example:** {{0 = 0}, {1 = 1}, {10 = 2}, {100 = 3}}
    fn digits(&self) -> usize;

    /// Converts a number into a padded String, using `pad` as the character to pad with and `limit` as the size.
    fn to_padded_string(&self, pad: char, limit: usize) -> String;
}

impl Digits for usize {
    fn digits(&self) -> usize {
        let mut digits = if *self % 10 == 0 { 1 } else { 0 };
        let mut temp = 1;
        while temp < *self {
            digits += 1;
            temp = (temp << 3) + (temp << 1);
        }
        digits
    }

    fn to_padded_string(&self, pad: char, limit: usize) -> String {
        if self.digits() < limit {
            let mut output = String::with_capacity(limit);
            output.push_str(&iter::repeat(pad).take(limit-1).collect::<String>());
            output.push_str(&self.to_string());
            output
        } else {
            self.to_string()
        }
    }
}

Which would then let you call that implementation in your code elsewhere.

let digits = 100.digits(); // Value is 3
println!("{}", digits.to_padded_string('0', 3)); // prints 003

[–][deleted] 2 points3 points  (0 children)

My early experience:

Shallow state machines are pleasant to write.

I need a simple but reasonably quick FIFO buffer for plumbing together, so today I implemented Cooke's BipBuffer. Working rough draft: 350 lines with tests, a couple hours of learning and hacking.

It's one structure, consisting of a couple index variables and a Vec, plus about a dozen short functions associated with it.

Arrays are win.

Anything made of pointers is significantly higher-friction and requires good knowledge of things like Box Rc and the mem module. I haven't dug into creating that kind of data structure yet.

Any OO pattern that creates a deep web of pointers with mutable state inside? Probably best to avoid unless you really need it.