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

I have 10 years of experience about Java, Python and PHP. And I have strong OOP skills. I think everything as objects and every object has a parent object in my mind. But I want to continue to my career as Rust developer. But going to functional programming from OOP is harder than I though. Because my brain is confusing. How can I reuse methods, how can I share properties, how can I extend them and create class hierarchy and the more important question is that is the inheritence necessary? If inheritence isn't necessary then how can I create model for things? Actually I don't know my real problem too. May be I miss some important issues about Rust, or I just need a little more practice etc... Please help me for finding my problem and solution... May be this question is nonsense but still I need help. Thanks.

Edit: Let me explain which topics I understood about Rust. Borrowing and ownership, mutability, if and match, loops, vectors, generics, creating a basic struct, creating a trait, implementing a trait for a struct, deriving a trait to a struct, enums, writing tests, writing comments for auto generating documentation. I only know these topics. Which topics I didn't understand? How and where to use the where keyword, async (actually I'm understanding a little bit this topic) and other advanced things.

Edit: Which tutorials I finished?

I can understand the lessons but still I'm not feeling myself comfortable when I create a project in Rust for myself. I hope I able to expleined my problem. Thanks for helping me.

5 Likes

For me, the biggest shift was changing my initial planning from "who is doing what" (like casting actors in a play) to "what data is grouped together".

I usually start with a struct with fields I think are related, then create free methods. When I notice a function using fields mainly from one struct, then I consider changing it to an method on that struct.
I'll continuously change the struct organization as more logic is implemented, and if the borrow checker nudges me what way.

10 Likes

With my teacher hat on, the best advice I can give for breaking out of a habit is "cold turkey". It's a common expression (in American English, anyway) that means quit smoking immediately instead of weening yourself off the drug slowly. [1]

You may have strong piano skills, but that doesn't automatically make you a strong violinist. [2] The observation is that these (languages, musical instruments) are very different tools, even though they can accomplish similar things (create programs, play music).

The answer might seem a little perplexing, but it's broadly "you don't!" Method reuse is provided by traits. Sharing properties is rejected outright [3]. Class hierarchies do not exist. Classical inheritance is no more necessary than the pure functional paradigm or async I/O or even threads for that matter! You can write a single-threaded program with blocking I/O and it will be just as correct as a multi-threaded async program. There are differences, I'm not arguing that, but I am making a point about what it means for something to be "necessary".

If I had to guess, I would say that you definitely do not understand ownership and borrowing. And that's ok! It will take you a very long time to get used to the nuance. That's just the nature of learning something new. These are deceptively difficult because the surface-level "rules" are simple, but they are interrelated and also somewhat independently deep topics. Multiple academic theses have been written on the correctness of the model, just to give you a taste for how deep the rabbit hole goes.

The allusion to The Matrix is intentional. If you think you understand borrowing, then you are still trapped in the Matrix, unable to see the real world that lies beyond it. In fact, you don't even know there is a real world. You can take the red pill, if you are interested...

But aside from that. I honestly have no idea what to suggest. You might be better served asking more specific questions when they come up as you are working on a project.


Come to think of it, these might help! They are allusions in the form of short stories. Entertaining, and good lessons: Rust Koans​​​​​ - The Rust Programming Language Forum (rust-lang.org)


  1. Cold turkey - Wikipedia ↩︎

  2. Is it easy to learn violin for pianists? : lingling40hrs (reddit.com) What, you thought I was joking? ↩︎

  3. There is ongoing discussion about code reuse by allowing fields in traits, but it's unlikely to be available in the near term. Maybe "pay no attention to the man behind the curtain" for now. Efficient code reuse · Issue #349 · rust-lang/rfcs (github.com) ↩︎

8 Likes

I'm so glad for your answer. Example of Matrix is awesome. I will deeply think and investigate about ownership and other suggestions again. May be I must make more projects and deeply understand other things.

I feel that Rust can open so much interesting doors in Matrix world, because of that there is something that attracts me in Rust. One day I will open my eyes and say that to Morpheus, I know Rust.

2 Likes

First, you can try to code a bit in the Elixir language -- it will give you a functional-like view on objects as structures + async methods transforms them. Then, Rust has something in its design close to this approach.

1 Like

If you accept a piece of advice: "deep thinking" about a very general issue such as ownership is unlikely to be fruitful. If you are not yet familiar with the concept, you'll probably be better served by writing actual code and trying to solve specific problems, and getting feedback for making your code more idiomatic.

There's one somewhat specific-ish question in your OP that can be answered somewhat simply:

There's no inheritance in Rust and it isn't necessary:

  • to represent a small closed set of strongly related types, you use an enum. A good example is serde_json::Value, which is conceptually one thing ("a value"), but concretely, it can be a number, a string, an array, etc.
  • To make functions and collections (and other types) work over an arbitrarily big set of potentially unrelated types, you make them generic. You use trait bounds to restrict the types to a subset with a given capability that you'll then be able to rely on in the implementation.
  • For type erasure and dynamic dispatch, you use dyn Trait.
14 Likes

Classes are a means to an end. You don't need to figure out how to extend structs in Rust to make them act like classes because that's not how you design stuff in Rust. "How do I do inheritance/share properties/extend structs/reuse methods in Rust" is a pure XY problem. The X is the problem you are trying to solve and classes/inheritance/properties/methods/structs/etc. are the Y. Everything you're trying to do in Y can and should be discarded if it doesn't help you solve X.

So. What are you trying to do? Not "I'm trying to create a model for things" but what things are you trying to model and why? Good engineering doesn't use the same techniques over and over for different problems just because they're familiar; it uses techniques that make sense in the context of the actual problem being solved. There is no good design that doesn't take into account what the design is for. There's no trick that will transform good Java code into good Rust code without regard for what the code does.

Go back to basics. The first step is to define your requirements. What kind of software are you trying to make? What does it need to do?

11 Likes

When I started using OOP many years ago, I used inheritance extensively to reuse fields and method implementations.

Over time, my style changed to use inheritance mostly to declare types: types to be inherited from are abstract and implementations are largely left to concrete leaf types. Reuse of implementations happens through means other than inheritance.

My old style would translate very poorly to Rust. My new style, on the other hand, works with Rust just fine.

4 Likes

So how can I separate same fields from multiple structs? For example I have a database and all tables have id, created_at, updated_at fields. How can I separate these fields?

struct User {
  id: u32,
  firstname: String,
  lastname: String,
  created_at: Date,
  updated_at: Date
}

struct Role {
  id: u32,
  name: String,
  status: ActivePassiveStatusEnum,
  created_at: Date,
  updated_at: Date
}


struct Books {
  id: u32,
  user_id: u32,
  name: String,
  isbn: String,
  status: ActivePassiveStatusEnum,
  created_at: Date,
  updated_at: Date
}

As you can see same fields exist on multiple structs. How can I reuse same fields?

In OOP, there is the concept "Composition over Inheritance" wikipedia.
The ideas there can help bring an inheritance-based design approach to one without inheritance. This can be done within an OOP-mindset. The composition-based design is then a better fit to be used in Rust, since Rust has no inheritance

4 Likes

What I would try first is separating out the data (things specific to your application domain) from the metadata — the parts that are about “this is an entity stored in a database record”. You can then define a generic struct for the metadata:

struct Record<D> {
    id: u32,
    created_at: Date,
    updated_at: Date,
    data: D,
}

struct User {
    firstname: String,
    lastname: String,
}

struct Role {
    name: String,
    status: ActivePassiveStatusEnum,
}

struct Books {
    user_id: u32,
    name: String,
    isbn: String,
    status: ActivePassiveStatusEnum,
}

and then use Record<User>, Record<Role>, etc. as what is passed to and from the database.

For some applications, it might make sense to put the composition relationship the other way around, which makes it more like classical OOP inheritance:

struct Metadata {
    id: u32,
    created_at: Date,
    updated_at: Date,
}

struct User {
    metadata: Metadata,
    firstname: String,
    lastname: String,
}

but this requires more boilerplate code to access each domain struct's metadata field, and it also is less flexible because it means you can never manipulate a domain struct without yet having the metadata (e.g. record about to be newly inserted, with no ID yet), so you should not choose this just because it is familiar.

27 Likes

This is what I would call a toy example, because (even if it's derived from a real-world problem) it doesn't come with enough context to understand what makes a good design. kpreid's suggestion is a good one, but it's not the only possibility. The problem is that toy examples don't have requirements, and so it's impossible to compare solutions. Even if you had classes, you wouldn't necessarily use them every time you have a problem that looks vaguely like this. Don't forget that sometimes the best solution to duplicated code is just to let it be duplicated.

You can't divorce the design of the solution from the problem that it solves. What problem is this code trying to solve?

6 Likes

One relatively easy comparison that you can make between Rust and a lot of OO languages is interfaces and traits. An object can implement multiple interfaces and interfaces just declare a set of methods that must be implemented by the class. This is much like traits in Rust except traits are more flexible.

Traits mostly just implement a set of methods but there are some more features. Also you can implement new traits on foreign types and have generic implementations and so on. Serde is a good example of this. It implements new traits like Serialize and Deserialize on standard library types like Vec and HashMap, adding new functionality to them. These implementations actually require the generics of these types to also implement Serialize or Deserialize. That means that you can Serialize a Vec<String> but not Vec<Instant>.

Other crates build on top of Serde by making generic functions like serde_json's from_str function.

1 Like

I can also recommend watching more advanced programmers solving problems - or even better: trying to solve them and then watching their solution after!
I did this with advent of code (which is great, as it starts with very simple and get's more complex):

  1. solve a day's puzzle (my repo)
  2. and then watch the solution and great explanations from a youtuber:

I've learned many things about 'the rust way' :smiling_face_with_three_hearts:

1 Like

Thank you all for your valuable messages. As I understand that we can make everything in Rust with different methods. For example we can use Hashmap for getting database data, we don't need to use struct for that.

As I understand there are some big differences between garbage collected languages and non-garbage collected. Probably my main problem is that I must spend more effort for understanding these differences. For example how organizing data and functions in memory (RAM), what is heap and stack and where they came from, why there are different kind of asynchronous implementations etc...

You don't need to, but modern usage of databases in a strongly-typed language means that you'd map your database entities to domain objects anyway, rather than using dynamically-typed name-value maps.

4 Likes

As you mention "database" and "model" perhaps this video presentation will give you some inspiration https://www.youtube.com/watch?v=z-0-bbc80JM. The presenter exactly discusses modelling data, not with classes and inheritance but with Rust enums and match statements.

It should be noted that enums in Rust are very different and far more powerful/expressive in Rust than the simple minded enum in C, C++ or other languages. The Rust match expressions enforce useful rules that ensure programming correctness. Which would have to be done manually by the programmer with if statements in other languages. Rust's enum and match work together to provide the data structure you want and enforce sensible rules on how your program can use the data. Which loos like it can do what you want, to model data, whilst ensuring constancy and correctness.

I think this is a case of quickly learning how Rust enum and match work, syntactically at least, without realising the power they can provide, in combination, to do what one wants in ways one is not used to. I think I have had that problem with many Rust features.

I imagine combining generics with and enums, match brings us to another world of things we can do in unusual ways. I'm not experienced enough to know yet...

2 Likes

Ironically, (given op mentions they know Java) java enums are pretty powerful, able to have constructors, methods, fields, overrides per variant, and so on. Similarly, it's fairly common in python to define "sentinel types" to represent values like None that can have alternative implementations, which is a similar functionality which simply doesn't often get used that way.

Neither of these quite capture the distinguishing clean functionality of Rust enums which is their heterogenous matching, of course.

It seems unsurprising to me that other language’s users have already used or constructed patterns similar to Rust’s enum types – to the degree the respective language allows – for a long time. After all, algebraic data types in (especially functional) programming languages are a >40 years old concept.

I think that phrase highlights the difference and you may contrast it with this:

You are talking “past” each other because you are discussing not just different ways of doing the same thing, but, in reality, you are talking about different, albeit related, activities.

That infamous Hoare's quote separates them like this: There are two ways of constructing a software design; one way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.

OOP is pinnacle of the latter approach and is often practised like a genetic programming: we start with some simulation of the product area, then improve that simulation iteratively till we are satisfied by the end result and then we can ship what we have and continue improving it. Tracing GC and isolated question how can I reuse same fields are perfectly reasonable in that approach: we don't have the end goal, we are just trying to improve what we have, why is it so hard to understand?

Rust, like Haskell, goes after Hoare Property: the end goal is correct, finished, program, not just reduction of number of bugs! This makes isolated “genetic” questions almost entirely pointless. If the change can not be justified by some “business case” then why do you want or need to do such change at all? What's the point?

One striking example is fate of GC in Rust. Not every newcomer even knows about that but Rust have started as typical GC-based language, and some find it even more puzzed how such an important tool could be just removed. Heck, even the blog post that announced the change had no plans to do something as drastic: GC was supposed to be moved to a separate library, not entirely removed… but if your goal is correct, finished program then GC is more of a nuisance than help. Yes, it helps to eliminate some bugs, but it brings a lot of complexity and doesn't solve the problem anyway. Layman-POV memory leaks are still easily possible—they are just reclassified as “endlessly growing data structures”.

Your main problem is still the fact that you try to find a way to “tweak code locally, then observe global effects from local changes”. That's not how Rust works at all.

Rather if you try to change something locally Rust helps you to propagate changes globally and thus go from one, correct and logical, program to another, correct and logical program.

That's something that makes it “the most loved language” but it's almost the exact opposite from what typical “OOP design” schools teach: they are, usually, go after flexibility and not after tight coupling which is typical in Rust.

7 Likes