What does "Rust & OOP" mean to you?

Hey all!

So, @carols10cents and I are working on the new version of "The Rust Programming Language", and it's time to work on a chapter we're currently calling "Rust and OOP". We're doing this because a ton of people have questions like "how do I oop in Rust", and so I want to make sure that it's actually going to address these kinds of things.

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

is the rough outline we came up with. What do you think? Are there other things we should cover? Is this useful? Should we totally re-do it somehow?

Please remember this is one chapter of a book, not a whole book, so we can't go super, super super in depth on many things.

Thanks! :heart: :blue_heart: :purple_heart: :heart:

25 Likes

This may be less universal than what you have in mind, but from my perspective Rust doesn't need OOP because the niche it operates in is better suited for things like data-oriented design. When I write Rust, I rarely think in terms of objects that share code or perform actions. Instead I think in terms of data to be transformed, at which point questions like delegation boilerplate simply disappear.

So I don't know how much space it would make sense to devote to this, but maybe include a section on alternative paradigms that just bypass OOP altogether? How to take a step back from the abstractions and solve the immediate problem at hand. When pushing data-oriented design in particular, for example, people often get hung up on a lot of the buzzwords they've picked up from OOP (encapsulation, open/closed, etc), without considering that maybe they're not universally applicable.

edit: One example of this I ran into recently is Cretonne vs LLVM. LLVM represents its IR with inheritance and pointer-based graphs; Cretonne replaces pointers with indices and in return is able to use multiple parallel tables of enums rather than inheritance.

16 Likes

I think you should come at it from a similar angle that Go does, since the languages are quite similar from an OOP point of view. It's OOP without inheritance. Delegation deserves to be higher up, but I would not mention deref for delegation at all since it's considered an anti-pattern

8 Likes

Copy requires Clone because Copy is a subset of Clone's behavior, since if you have one, you can trivially implement the other.

I don't know how relevant it is to this chapter, but I think that line is incomplete without adding "and coherence rules prevent us from having a blanket Clone impl for all Copy types." (Niko has written about that one here: Intersection Impls ยท baby steps)

I find coherence rules to be one of the hardest parts of Rust to understand. First, the errors can be confusing. ("Why does the compiler think there are conflicting implementations?" "Well the problem is that there could be conflicting implementations in the future. You can do what you're doing with your own types, but not with other crates' types.") But maybe equally important, there are Deep and Fascinating Reasons why the rules are the way they are, and the places that explain the rules usually don't touch on those underlying reasons or the problems they're solving. Similarly, it would be great if the documentation for traits like AsRef and Borrow explained why they have different blanket impls, and how that choice was guided by the coherence rules, and how all of this is expected to evolve in the future. Explanations of this stuff are pretty hard to find.

7 Likes

Might be worth mentioning that the visitor pattern is often more idiomatic as an enum + match and have an example. Unsure if this tidbit really fits the chapter or not.

4 Likes

I may not have communicated this effectively, but this is the basic idea of the chapter, yes. :smile:

3 Likes

There's a whole section on coherence in chapter 20. This is extremely well said though, I'll add it to my notes, thanks :smile:

2 Likes

Regarding delegation, I think it would be worthwhile to mention AsRef/AsMut. (E.g. if you have n traits that are implemented for AsRef, you only have to implement AsRef for Bar)

1 Like

I feel that for the chapter to be effective it should have examples that cover the space of use cases for inheritance, and in each show how Rust tackles that use case. Most of them should be examples where inheritance is a proper solution, but the chapter should deive the point home by finishing up with an example or two where inheritance really isn't a proper solution and proper composition is nicely encouraged by Rust's way of thinking.

If there is something that OOP languages can do (easily) and Rust can not (easily) that should also definitely be made explicit, to prevent people from getting lost in the woods of awkward solutions.

12 Likes

Copy and Clone need explaining somewhere, but using them as an example for supertraits seems overcomplicated.

Could you provide an example with simpler supertraits?

That would also provide a good opportunity to show the difference between supertraits and additional bounds on methods, and how Rust objects can have methods with different requirements than the overall object. For instance, Vec::dedup requires PartialEq, and Vec::sort requires Ord, but Vec itself doesn't require either one. Introducing that pattern also helps get people used to the differences between traits/impls and traditional OOP methods.

I would also suggest explicitly discussing the difference between interface-based OO and implementation-based OO, showing how Rust implements the former directly (with some additional features like the above), and showing examples and patterns for the latter (the most common being functions that operate on a trait the object can implement, but has-a and generics can also work for patterns that other languages do with inheritance).

4 Likes

Few points:

Biggest Rust transition I had was the "I know how to do X in Ruby, etc., how do I...". Problem.

Looking at a typical OOP design for a non-trivial problem, and translating that to the Ruby space led to lots of headache with Trait objects and Box etc. Never did figure out how to do static dispatch without leaky abstractions.

Swift/Apple have done a good job with "Protocol Oriented Programming" -- establishing that as the preferred design pattern with protocols over value types. They have yet to get UIKit and ObjC interop to the place where it plays nicely with Protocol Oriented Programming, but have actively evangelized a clear alternative design pattern. (It does help that due to the way Swift manages the performance/ergonomic tradeoff, dealing with protocol objects is much more painless than with trait objects & Rust).

It's not clear what the "Protocol Oriented Programming" equivalent is with Rust... Doing some work to think about how to model non-trivial problems in Rust might be helpful. Looking at example libraries or applications, working through an example that requires design, these all help make the transition....

3 Likes

This is exactly what we're trying to solve with this chapter. How to help people get over this gap.

1 Like

To me, I do not think of Rust at all in terms of OO, but more as kind of it's own thing (or at least similar more to Haskell's approach of interfaces to types), but I might not be the target audience (I generally avoid OO, or at least prefer lighterweight approaches like JS').

From @steveklabnik's comments though, that sounds like the idea is to explain this, ie. rather than explaining "how to do the Builder pattern in Rust", the aim is more "Rust works different, here's how we might accomplish the same goal with types and traits", which sounds quite useful both in terms of introducing the new approach for those unfamiliar, and even to weirdos like me who might want to port something written in standard OO style to something more Rusty.

12 Likes

Yes, @jarcane, you nailed it.

When I think of OOP in Rust, I think of CLOS (Lisp) - I really like that approach.

And, even though my Rust is rusty (harh, harh), isn't that how OOP works with Rust?

"Land Of Lisp" by Conrad Barski is a great book! :relaxed:
When he demonstrated how to do OOP with monsters, it blew my mind, because it was much more flexible and cleaner than C++.

(defstruct monster (health (randval 10)))

(defmethod monster-hit (m x)
  (decf (monster-health m) x)
  (if (monster-dead m)
      (progn (princ "You killed the ")
             (princ (type-of m))
             (princ "! "))
    (progn (princ "You hit the ")
           (princ (type-of m))
           (princ ", knocking off ")
           (princ x)
           (princ " health points! "))))

(defmethod monster-show (m)
  (princ "A fierce ")
  (princ (type-of m)))

(defmethod monster-attack (m))

(defstruct (orc (:include monster)) (club-level (randval 8)))

(push #'make-orc *monster-builders*)

(defmethod monster-show ((m orc))
  (princ "A wicked orc with a level ")
  (princ (orc-club-level m))
  (princ " club"))

(defmethod monster-attack ((m orc))
  (let ((x (randval (orc-club-level m))))
    (princ "An orc swings his club at you and knocks off ")
    (princ x)
    (princ " of your health points. ")
    (decf *player-health* x)))

(defstruct (hydra (:include monster)))

http://landoflisp.com/orc-battle.lisp

2 Likes

Just one rando's opinion: i think an end-to-end worked example that compares and contrasts a traditional OOP implementation and an idiomatic Rust implementation of the same problem would go a long way (for me, at least).

11 Likes

[quote="steveklabnik, post:1, topic:9633, full:true"]
So, @carols10cents and I are working on the new version of "The Rust Programming Language"[/quote]

Just wanted to say that other programming languages are mighty jealous that Rust has got such great a book! :slight_smile:

9 Likes

Pure OOP is not necessarily as popular as it used to be. Advanced OOP with complicated layers of inheritance just doesn't seem to be that common and many times I've seen it derided. In .Net and JavaScript, at least, there's often a lot more focus on open/closed principle, separation of concerns, and inversion of control - means to keep components separate so that they can be easily exchanged and tested.

Inversion of control tends to rely on a) interfaces and b) dependency injection frameworks. I would find a section explaining how to swap components in Rust very useful, if such a thing is even possible. In other words, declare that I want a certain kind of Thing, and then be able to swap out various Things without specifying exactly what they are, including mocks.

Similarly, is it possible to build a DI framework in Rust? Looks like someone tried to design one here and perhaps this could showcase some potential problems: Dependency injection container - Learning the ropes in Rust

I don't really know if this addresses the topic, though, as I'm not really clear if the concern here is raw OOP in particular (which may potentially be needed for, say, game engines), or the kinds of design patterns that are often favored in the enterprise world.

9 Likes

The big idea that I think people should understand is the difference between:

  • Behavioral abstraction / Polymorphism - how a procedure can operate over values of different types.
  • Data composition - how each type is assembled from primitive types.
  • Procedural composition / Code reuse - how each procedure is assembled of subprocedures.

Inheritance couples these three concepts into a single expression. In Rust, these are handled separately:

  • Behavioral abstraction is provided through traits and generics.
  • Data composition is provided through structs and enums.
  • We have weak inherent support for code reuse; nothing like super or built in delegation (yet). Fortunately this is just sugar for the most part, you can always just call the function you want to delegate to.
19 Likes

I guess we provide code reuse through things like blanket impls and default methods. Code reuse is sort of hard to disentangle from behavioral abstraction. I think the big thing I like about Rust's system over traditional OO is the disconnection between data composition & behavioral abstraction.

1 Like