I really don't know how to program

The issue is more nuanced than that. It just cannot outlive its owner.

The most common problem of this sort is when someone inappropriately attempts to persist references across frames while returning to the caller. This is "obviously" in error, but it's so common that it makes you wonder if it may not be as obvious as it seems.

In the sample code you provided, the view struct is alive for the entire runtime of the program, but it's OK because it doesn't outlive the owners. Ergo there is not always a problem with it.

Sorry, I exaggerated the problem because I wasn't specific. But just so you're aware, it would occur if you return references from methods in Ctx and then attempt to do mutations. For example, this fails:

            impl<'a> Ctx<'a> {
                fn get_data(&self, key: &str) -> Option<&String> {
                    self.store.map.get(key)
                }
                fn dothis(&mut self) {
                    if let Some(data) = self.get_data("key") {
                        self.dothat(data);
                    }
                }
                fn dothat(&mut self, data: &String) {
                    // ...
                }
            }

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/lib.rs:71:25
   |
70 |                     if let Some(data) = self.get_data("key") {
   |                                         ---- immutable borrow occurs here
71 |                         self.dothat(data);
   |                         ^^^^^------^^^^^^
   |                         |    |
   |                         |    immutable borrow later used by call
   |                         mutable borrow occurs here

That's a contrived example, but hopefully you see the general issue. If methods with Ctx as self return a reference, they borrow Ctx and prevent further mutations, which is the similar to the problems you originally ran into. Even if you do put everything into Ctx, I recommend putting as many methods as possible where they logically belong on the individual sub-objects.

Yes, that's what makes it tempting to use a God object as @khimru pointed out. The problems with doing this come later as the program grows. You may run into borrowing problems like above, or refactoring may be more difficult because a large part of your code will depend on having access to all fields in Ctx. The more you can separate data and methods by responsibility, the better.

Definitely! And I probably will split mutables from immutables.

The main thing is... There must be some central handling mechnism available which can "do things" we are not interested in on a detail level.
For example deep down a character is dying.
Somewhere up the hierarchy there must be something which can play a sound or send some message to play a sound.

The most logical way seems for me:

if character.is_dying
{
    ctx.cue_sound_effect(enum::diesound);
}

or

if character.is_dying
{
      ctx.handle_message(enum::character_dying)
}

isn't it?

(it is a simple game BTW)

I definitely see your point. I haven't written games, so someone else who has could probably give better advice. I might use queues/channels for that sort of thing, but I don't know how it's best done with games. The key thing with Rust is that you wouldn't want to have anything in Ctx borrowed at the time you actually do those operations, which is one reason that queuing them comes to mind.

1 Like

I don't see fabled layers here, sorry. What this code does is glorified goto: it directly jumps from the layer which is responsible of keeping track of character's state to layer which plays sound.

Looking back on an epidemy of OOP that took the programming world by storm in 1990th it really feels, from today, as “revenge of goto programmers”. I think it's not a coincidence that OOP was born at the same time as as proclamation Go To Statement Considered Harmful have started being taken seriously.

In an era of Wheeler Jump GoTo from the middle of one fortran procedure to the middle of another one wasn't unusual and in earliest version of FORTRAN that started optionally supporting call stack was FORTRAN 77 with FORTAN 90 it, finally, have become mandatory.

And, of course, when pressure was mounting to stop writing spaghetti code and start using structured programming people were looking for the way to, somehow, transfer their habits into this brave new world of self-contained procedures and functions.

OOP was found the easiest way to transform one mess into another: now, instead of spaghetti code we have very similar soup of pointers and can faithfully recreate identical mess without dreaded goto word. Ain't that grand?

Well, Rust doesn't consider it grand and while it does provide a way to reproduce “soup of pointers” designs to ensure people with years invested in these would be able to adopt it, too — it exposes it and makes it visible.

Okay, then, if we want to avoid these jumps from one layer to another then… how do we do that? Traditional answer in gamedev is ECS, in CRUD apps it's MVC and so on.

Instead of making random connections between layers you centralize the flow of data from one layer to another and add a message pump.

And that's it: no more hidden connections between layers, no trouble with addition of “silent killer” to the game and… suddenly, no trouble with expression of these ideas in Rust.

Note that all these techniques, while embraced by Rust — they weren't designed specifically for Rust.

They just tried to solve the same issue: reduce amount of hidden dataflow of state between layers, which leads to problems both with design and, especially, with debugging.

It doesn't always work perfectly, some designs are, indeed, better expressed with soup of pointers than with something else, but when I see that

if characater.is_dying { /* do something at etirely different level */ }

I cringe: that's not “many different levels”, that one big fat mess, you may as well just put all your variables in globals and stop pretending that you are doing modular design.

Why do you even have context, many different levels and all these things? Why even bother? What are you planning to achieve?

If you gave is too simple to invest into proper ECS then why do you even bother to introduce all these layers and pass them around if you may just put everything in one big global state?

And that's exactly what gamedev ended up inventing. Years before Rust was even dreamed of, mind you! They weren't trying to make borrow checker happy, though, that was decades before borrow checker even existed.

But they, too, suffered from issues with shared mutability where killed character was trying to do something with killer and then, if two characters killed each other simultaenously you ended up with some crazy state and unforeseen consequences if not outright eneless recursion and crashing game.

2 Likes

True, and I cannot see anything wrong with that. Your words sound a bit angry to me.
Whatever we program: it is always a jump from a to b. And however we program it: there always can and will be bugs.

Why do you even have context, many different levels and all these things

I could write everything in one function of 50.000 lines. No problem.
I am not a professional game-developer. I program for work, but this is hobby. I never looked at ECS etc.
The hobby is more difficult than the work actually.

BTW: I always hated OOP. I have only - almost without exception - witnessed miserable drama's.
That is the second reason I like Rust: no (old) OOP. There will be more I hope.

in Rust, a "view struct" typically refers to a struct that provides a view or a subset of data from another struct or data source. View structs are often used to encapsulate and provide access to specific parts of a larger data structure without exposing the entire structure or duplicating data.

For example, you might have a large struct representing a complex data entity, and you want to provide a simplified interface or access to only certain fields of that struct. You can create a view struct that contains references to the original struct or borrows data from it, allowing you to interact with a subset of the data.

View structs can help improve code organization, encapsulation, and readability by providing clear and focused views of data within a larger context.
Chat GPT :slight_smile:

I am very thankful for all responses.

1 Like

Because the whole discussion sounds like a complaint about how you tried to use a telescope to hammer nails and found that it wasn't very convenient in that role.

Yes. But that ability, while important, have also caused a software crisis because unfettered jumps lead to unmaintanable and unfixable code (cue in Mel the Real Programmer, who was asked to make game cheat, was unable to do that and then bragged that his subconscious was uncontrollably ethical, and adamantly refused to fix it).

One step that was accepted by mainstream was structured programming, which was quite a drama to make people accpt. And many only accepted it when they found convenient way to move the whole mess from code into data structures.

Since then many languages tackled that issue and placed more and more restrictions on how you can structure the code and data. Except structure of data as “soup of pointers” was considered the pinacle of mainstream language development for two or three decades and additional restrictions were invariably handled by libraries that you may use or not use.

Sure, but the question is about how many bugs would be there and how easy would it be to fix them.

Rust is based on the idea that restricting programmer to reduce number of bugs is worthwhile and it, finally, moves the needle and restricts not just the code, but data structures, too.

And you complain that you don't like these restrictions and demand your freedom to create soup of pointers back… but why? What's the point?

If you don't need need increased safety, stability and debuggability (that are bought by quite complicated Rust design and restirctions it places on “soup of pointers”), then there are many other languages which wouldn't restrict you so severely! And many are even used in gamedev and have extensive libraries which help one to write games!

Why invest in the telescope if you don't plan to ever use it as anything else but the hammer? And, consequently, why assume that others would be willing to dismantle their telescope to help you hammer nails?

I know you're just innocently posting some info about this, but using ChatGPT is probably a mistake. None of what it wrote about it is accurate, or at least does not apply to the "view struct" in the blog article that we've been discussing. The blog article describes it specifically as a workaround for borrowing issues. Unfortunately "view struct" is a fairly general term so I'm afraid that ChatGPT is using the wrong sources or over generalizing.

I don't know what the policy on the posting of AI generated content here, but there have been some posts that were flagged because of it. Anyone know?

1 Like

Because the whole discussion sounds like a complaint about how you tried...

I am learning the language. Everybody knows how frustrating it can be doing that. I am sorry if it sounds like complaining.

1 Like

You're fine. :slight_smile: The problems you're having are not unique to you. There is a reason that Niko's "view struct" approach was conceived and has been recommended here. It is true that learning Rust requires reorientation and thinking differently, but you can do that gradually and in your own way.

1 Like

True. GPT has cost me many hours instead of winning them... Though it was indeed an innocent naive and i thought funny quote.

1 Like

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.