I'm still thinking OOP in Rust, please help me for escape

Well, it seems like you've got a great and fantastic path into programming with those amazing tools. And if you value OOP that's fantastic as well!
But from my own experience with those languages and others, at first I tried to learn Rust comparatively, and that won't work, it's like someone already said: piano is not violin!
So, Rust is not a language you can switch from in a weekend of readings, so I recommend you some of the books in the order that worked for me:
(I won't provide link to paid sources so that you don't think I am monetizing my intentions here - feel free to search for them at will - some books are web biased, but it seems okay, since you'll learn a lot of Rust anyways)

*** I am not a native English speaker, so I'll try to communicate the best I can:

0 - Rust by Example (free online) Introduction - Rust By Example
My point: Use this book as an "exposure", no string attached, just to learn what Rust can do.
1 - The Rust Programming Book (2nd edition) (free online) The Rust Programming Language - The Rust Programming Language
My point: Use this book as a "hands-ons", STILL no strings attached, just to cement the previous one
2 - Rust in Action - Systems programming concepts and techniques by Tim MacNamara (paid on Manning)
My point: Use this book to understand why Rust is different at all in what it's best for.
3 - Rust for Rustaceans - Idiomatic programming for experienced developers by John Gjengset) (paid on No Starch Press)
My point: Use this book to start understanding how to "think as" a person that's Rust biased
4 - Code Like a Pro in Rust by Brendem Matthews (paid on Manning)
My point: Use this book to build a mind as a "professional" that uses Rust.
5 - Rust Servers, Services, and Apps by Prabhu Eshwarla (paid on Manning)
My point: Use this book as an abstraction to what you already know and have a different view of everything until now.
6 - Zero to Production in Rust - an opinionated introduction to backend development by Luca Palmieri (Paid on Zero2Prod)
My Point: Use this book to fill that gap: "Ok, how I use rust in production"
7 - Black Hat Rust - Applied offensive security with the Rust programming language by Sylvain Kerkour (Paid on his personal website)
My Point: Use this book to build a new level on your Rust carreer!

Hope you can purchased those books from original sources!

5 Likes

Rust itself is not born for objects, but for safety and efficiency. In terms of object orientation, Rust believes that composition is superior to inheritance, so it does not support inheritance and can only approximate inheritance through the combination of multiple crates. Past knowledge and habits may be baggage, inheritance conforms to natural human thinking but brings complexity and overhead, which is not in line with Rust's original intention. Therefore, embrace change.

1 Like

In the era without C++, it was possible to write perfect programs using C and assembly, but it might take a little longer. Therefore, people are more important than programming languages. Different languages can be used in different fields. For example, Python is preferred for data processing, and Rust is now better than C++ for system-level. And by combining different languages, using ChatGPT and other tools to quickly rewrite code, each language can show its strengths and assemble into a more stable and secure software system.

1 Like

Yes! Object orientation is not all your need!

1 Like

I was hoping to see someone chimed in with this. I was a bit surprised at all the comments where nobody mentioned the shift in thinking needed from a “this IS A foo” to “this HAS A foo”. Thank you for speaking up and responding

1 Like

There are so many good responses, I feel as if I can't offer anything of value to contribute. But, on the whim that it is helpful, I have learned something from programming in Forth which has helped me with my Rust programming.

Try as hard as you can to stop thinking in terms of anthropomorphic entities ("objects"), and instead focus on conceptual entities. Then, formulate your design/solution in terms of those concepts.

For example, if you're modeling a game, you have players, opponents, probably have visual effects like sparks, and of course a score. In a perfect world, your video hardware would express each of these as sprites, so it would make sense to also model them as sprites. A sprite has a left coordinate, a top coordinate, maybe some Z-axis information if it's necessary, etc. It could also have a callback to a paint function if it makes sense for the application.

One thing to note though is that each sprite would have an id field. This field is not the identifier for "the sprite." Rather, it is a foreign key (in relational DB terms) to the entity which is sprite-like.

So, you'd have a module "sprite" that implemented the structures, procedures, and impl-methods for this concept to work. And as long as you can guarantee unique ids amongst all of your other application entities, you can make literally anything sprite-like.

As you can imagine, this can be used for pretty much anything, and with any level of granularity.

Now if you take this concept and optimize it for actual run-time performance, you get what's called "Data-Oriented Design" or "entity-component systems". But, if you don't care about performance (and, yes, this style of programming can sometimes be slower than OOP, etc.; as long as it runs fast enough to be useful, that's all that matters), you can find that this approach towards modeling can be useful, and occasionally even more powerful than most OOP paradigms. Multiple inheritance, dynamic classes, etc. are all easily "implemented"/emulated with this approach simply by enlisting and delisting "entities" with different concepts at different times.

I apologize for how abstract this may seem, especially since I don't give any concrete examples. Once upon a time, my plan was to write a book on this one day, but (a) nobody codes in Forth, and (b) I later perceived that ECS systems are already a well-known topic, so that thoroughly demotivated me from doing so. But, as a design approach, the topics covered by ECS-like systems transcends specific language.

Hoping this is at all helpful.

1 Like

Sadly I never did get on with Forth. So on reading that I thought "What? How can that be possible? But you make good points.

Only one point there I think is debatable:

I don't have much experience of this but many have said that ECS as you describe is preferable for performance on modern machines that have large cache memories. The argument being that data is more often found in cache with ECS.

1 Like

Ooh, maybe I didn't express myself clearly. Almost a certainty, as I don't have examples.

With ECS and DOD style coding, you often find that the entity IDs are treated directly as indices into vectors, which directly locates relevant data. That is extremely cache friendly, and certainly corroborates your understanding. However, this can lead to sparse arrays; you are ensuring rapid access to data by sacrificing memory consumption.

My approach tended to rely less on direct addressing and more on stating the relationships between different aspects of an entity. I was directly inspired by relational algebra when I came up with the idea. As a result, I didn't consider a flat array as a hard requirement for storing important data. Sometimes a B-tree was more appropriate or, even in the worst cases, perhaps off-line storage was used.

There is of course a tradeoff here; if you want rapidity of access to data, you might end up wasting slots in vectors because not every slot is going to be used. However, addressing will be very rapid indeed. This is often the case in video games, where rapid access is king. With my approach, you must scan through your data structure (which might not always be a vector) to locate a record by ID first. Applications written in Forth tend to optimize memory frugality over speed (consider most Forth systems are for embedded applications), so compact representation is emphasized over rapid access.

If I weren't such a case study in ADHD, I'd sit down for longer than a day and actually focus on documenting my thoughts more, and provide more concrete examples.

If I understood what you're struggling with correctly, I'd say Traits and Composition are the answer, but there's no inheritance like in OOP.

If you want to share a method, that's a trait, every time you want to reuse that method you derive that trait. That is assuming it even makes sense to make it a method to begin with, the OOP mindset conditions us to write methods all the time because we're usually working in the context of a class, but a lot of the time you could just write a normal function and reuse it.

I tend to think mostly on what the public API of the module I'm writing looks like, how other components are going to interact with it, if other modules could have similar behaviour then I create traits for that. Attributes are rarely relevant to me in this context as I tend to only interact with them internally on the type implementation, and don't expose them publicly.

I'm new to Rust myself, that's just how I'm trying to wrap my head around it.

How many lines is the largest Rust code you have written? One approach may be to just start a big project, write it the most intuitive way (OOP in this case), at some point, you run into barriers -- and use those barriers to guide you to change style from OOP based to trait based.

1 Like

I basically did exactly this. I had developed and maintained a python 3D OpenGL module pi3d and decided to re-write it in rust GitHub - paddywwoof/rust_pi3d: translation of pi3d from python to rust

I immediately hit the OOP problem: we had a Shape object that had all the basic functionality for transforming, rendering etc. Then we had Plane, Cuboid, Cylinder etc that inherited the core properties and methods, added a few more then had a specific constructor to generate the relevant mesh.

It took me quite a while (including complicated Trait systems and horrible macros) to figure that the core parent Shape object was simply something that was a component of each child object.

let my_cylinder = Cylinder::new();
...
my_cylinder.draw()
// became
my_cylinder.shape.draw()
1 Like

Interesting. I was expecting something like:

Then I realized Shape was to be a trait, and we did

trait Shape { ... }
impl Shape for Plane { ... }
impl Shape for Cuboid { ... }
impl Shape for Cylinder { ... }

I am interested to hear what led to the "shape is a piece of data in Plane/Cuboid/Cylinder" solution.

Can't remember the full details but the problem was that Traits only deal with methods, so all the attributes of shape were needed in each child object. The nicest way to do that was include Shape struct as a component. The trait implementation then becomes a wrapper for Shape::draw()

ah, things like obj center, fg/bg color, whether outline is drawn? Makes sense now.

I've just remembered a couple of other "moments of enlightenment": one was when trying to translate the python style constructors to rust. Traditionally there is a massive list of arguments with the majority all being set to some default value. The standard suggestion is to use a factory and/or implement Default and/or pass Option<> values as arguments.

In the end I realised that if Shape had methods set_position, set_shader etc. etc. then these properties shouldn't be set in the constructor at all. The constructor should only set the absolutely essential properties and everything else can be set in a list of method calls, which ends up looking like a factory system when you chain the method calls putting each one on a new line

    let mut cube = Cuboid::new();
    cube.set_location(&[0.0, 0.0, 5.0]);
    cube.set_shader(&shader);
    cube.set_fog(&fog_color, fog_dist, fog_alpha);
...

The other realization was that almost every ownership/lifetime nightmare (the camera objects needed to be mutable but I wanted to give references to different shape objects) could be solved using reference counting. Most of the time ref counting doesn't cause undue work or delays or have the overheads of a fully garbage collected language.

@emirbugra There are a lot of good answers already, but I thought a condensed form of it might help you, too, so here we go :slightly_smiling_face:

  • Data is almost always distinct from one another
    • you model them by using structs and enums
    • if you need to share common data, do it via composition
  • Behaviour is often common between data
    • you model it via Traits and their methods on structs and enums

Thinking of data being distinct and behaviour being common has really helped me in designing ergonomic software in Rust. In the end, your whole program becomes a big list of data transformations by invoking behaviour on that data (if you squint hard enough, every software is a compiler of some sort)
.
I hope, this helps. :slightly_smiling_face:

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.