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.
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.
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
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.
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.
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).
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…
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.
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).
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.
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.
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.
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.