What does "Rust & OOP" mean to you?

I think there's a definite trend away from deep inheritance since the basic tools of OOP make it so easy to build faulty abstractions.

IoC is interesting, C# is the only OOP language I know so will look at that. In the kind of .NET apps I've been involved in (relatively small) you only ever have a single implementation of a component and the container is just specifying how and when it should be constructed, and ensuring its dependents get it. It's a bag of types and constraints. The IoC principle is good, but sometimes the runtime behaviour of your container of choice can be unexpected. So in addition to mocks you also need good integration tests that use your live container too.

I think in Rust it's possible to do IoC with less infrastructure than .NET because we've got a stronger type system that can express dependencies without needing a framework to fill in the gaps. Ultimately though, without garbage collection I think IoC as seen in .NET is very challenging (I don't miss it, though).

1 Like

This post does a really amazing job of describing my views regarding object oriented programming.

OOP isn't about classes. It isn't about inheritance. It isn't about polymorphism. It isn't about the Liskov Substitutability Principle.

OOP is about objects, and objects are bundles of state and behavior. And anything that only has one is a poorly-designed object.

Inheritance, meanwhile, tends to encourage creating classes which describe poorly-designed objects - ones with no state at all and only behavior (comparator classes are a plague), or only state and no behavior to be seen (having one class per Job type, as described in the post.)

"Rust & OOP", to me, thus means "the tools Rust gives me for smithing state and behavior into nice, coherent little bundles" - and for this, traits/typeclasses are the single best OOP tool I've used. (Rust introduced me to them, but there are some fascinating things you can do with nullary typeclasses, or partially-applied typeclasses, which Rust does not support).

(I also posted this response in the reddit thread)

10 Likes

Thinking in types seems to be my biggest hurdle with Rust. New people to statically typed languages might try to interpret them as classes in OOP, although types operate more as behavioral interfaces than as classes. In Rust impl defines instance behavior, struct defines state, and trait defines the type behavior (sort of like decorators). Explaining this juxtaposed with classes/OOP structures might be helpful. Using instance methods and class methods to explain stack and heap concepts might be interesting too. :slight_smile:

3 Likes

There are no problems for me with OOP in Rust, because I don't consider OOP to be a language feature. It's more like collection of tools to me, and if one thing from the toolbox is missing, it's not a big deal.

I think OOP has sold a big promise it could not live up to. It promised magic. For example, we imagined that we could somehow join a wheel, engine and frame objects and they would magically work together. Except, however, the detail was precisely not in those objects, but in the way they were joined and the different contexts in which they are joined.

Big shift in thinking is to give up the pursuit of this magic. Instead of creating systems that look nice from the outside, but are a mess in the inside, it may be better to pursue simplicity and clarity throughout, at every level. And it requires looking at OOP as a toolset.

(condensed earlier post a bit because it was tad too long)

2 Likes

Though people have commented that OOP doesn't mean classes, I think a lot of people (including myself at times) associate OOP with creating and manipulating classes. Part of this chapter (or another chapter somewhere) should probably address why Rust doesn't have classes and why that's okay. It should explain why the Rust way might be better than what they've seen before in other languages with classes and inheritance (think Java, C++ and Python).

5 Likes

The big win for me is rust's explicit separation of behavior (traits) and data (structs and enums).

The way people tend to think of oop is typically informed by their experience with it as implemented in a previous language. This is why classes are synonymous with oop for some. Classes (mixed data and behavior) are an impl detail. The major design problems with that impl detail were almost entirely due to mixture that just never have been mixed.

Rusts design patterns in my mind provide a more suitable set of tools to solve "the expression problem" and other types of oop design issues where classes fall short.

2 Likes

To me Rust is not an OO language. It can achieve many similar things, sometimes using analogous concepts, but looking at Rust from OOP lens gives weird and distorted picture.

Traits and interfaces are kinda similar (if only they were called Copyable & Cloneable ;)), but trait inheritance is not really OO inheritance in any useful sense. Struct + impl is kinda like a class, and it's nice to have a method syntax and a bit of encapsulation, but it's going to disappoint fans of GoF Programming Patterns.

Traits and type inference create a new kind of polymorphism not limited to objects, but over any types used anywhere.

For me OOP is strictly "hierarchical" in nature, and Rust is "flat". It works better for me to treat Rust as C with lots of syntax sugar, rather than as Java with broken inheritance.

5 Likes

A few thoughts, in no particular order:

  • I like the idea to mention that enum solves the visitor problem but way better :slight_smile:
  • No class keyword? For C# programmers, a struct is on the stack and a class is on the heap. But in rust everything can go on the stack, so no class keyword. A "class Foo" is thus sortof an Option<Box<Foo>> (modulo sharing restrictions)
  • Why aren't my methods inside the struct? Point them back to inherent impls. Rust "private" (as opposed to pub) is then more like what C# programmers consider internal (I think the Java equivalent is "package private"?), but to the module (not the crate, which is more assembly-like).
  • What about all the things I can do to anything because they're on object? Those are separate traits for better efficiency and to avoid NotSupportedException or silly default implementations. Maybe point to PartialEq, Hash, and Display.
  • Oh, so a trait is like an interface? Well, sorta. Might need to mention downcasting restrictions on trait objects when talking about supertraits.
  • What about implementation inheritance? Delegation, default methods on traits (since interfaces usually don't have that).
  • +1 to whoever mentioned DI. That's basically all of how I use C# "OOP stuff" these days
  • Maybe something about how you'd do plugins? (Which I guess are sortof the contrapositive of DI.)
  • Plus some stuff from the dynamically-typed side of the OOP house...
2 Likes

One thing that might be worth mentioning is using dispatch on enum to achieve polymorphism with a closed hierarchy. I use this pattern in encoding_rs, but it doesn't make Rust look good, because one would hope the language to take care of stuff like dispatch, but instead, there's a script to generate the boring parts.

The benefits compared to trait objects are:

  1. Users of the crate are prevented from implementing additions to the hierarchy (there's no trait for them to implement), so there's type-safety in the sense that if you obtain a &'static Encoding, you know that it only exhibits behaviors that encodings specified in the Encoding Standard have. That is, a &'static Encoding guarantees that you aren't dealing with UTF-7, EBCDIC or something like that.

  2. The sizes of all the variants are known at crate compilation time, so whereas a trait object is unsized, objects with enum-based dispatch have a known size and can be allocated on the stack.

1 Like

Another interesting example is how static polymorphism via traits with associated functions (Default / FromIterator) help solving problems which require factories (java.util.stream.Collectors) or "duck typing"/reflection (default construable in C++ or in Java DI) in OOP languages.

1 Like

Plus some stuff from the dynamically-typed side of the OOP house...

Extension traits as monkey patching?

To me, it means using types as an element of a state machine. Where in more traditional OO languages, I would have single classes that would move through stages of growth by having fields start null and become populated, in Rust I just have multiple related structs, and each stage of growth returns the next struct in the line.

For example, FinalStruct::begin() -> FinalStructStage1; makes an object with what state I have available at the beginning, then as I grow more information I have a FinalStructStage1::grow(self) -> FinalStructStage2; method, rinse and repeat until FinalStructStageN::finish(self) -> FinalStruct;

The type checker therefore makes it impossible for me to have invalid object states, and I can tell by simple inspection where in the growth cycle I am at any point.

The only downside is that, in order to prevent a flood of intermediate types all with public visibility, I've been using a super-struct with a private enum member that unifies the intermediate types, and all the intermediate growth methods implemented on the super-struct simply match on the current enum state to delegate or fail.

It'd be nice if Rust had "Voldemort types", where methods returning private symbols couldn't be bound directly, but could have methods called on them, until the chain eventually did return a public type.

pub struct Final {}
struct Stage0 {}
struct Stage1 {}

impl Final { pub fn begin() -> Stage0 { Stage0 {} } }
impl Stage0 { pub fn grow(self) -> Stage1 { Stage1 {} } }
impl Stage1 { pub fn finish(self) -> Final { Final {} } }

let f = Final::begin(); // error: f is of type Stage0, which is not public
let f = Final::begin().grow(); // error: f is of type Stage1, which is not public
let f = Final::begin().grow().finish(); // ok: f is of type Final, which is public

I've been using this pattern to great success in my personal projects, where useful methods are only implemented on the complete type, but since I need to grow that type as information comes in, I have a type-checked state machine ensuring that each step is correct and I can only act on the finalized data.

http://rust-lang.github.io/book/ch17-00-oop.html

404 File not found

Since 5 days ago the new book lives in the "second-edition" subdirectory
http://rust-lang.github.io/book/second-edition/ch17-00-oop.html

It means the link in the opening post should be updated.