of course this is stupid code. but i really need to handle these things.
Again staring at the abyss of impossibilities... and again contemplating C# again.
I need some way to centrally handle things logically.
I have a struct with other structs which can do things. load, fetch, get, init.
Separately they work perfect. Centrally it does not work at all.
I do not want to make everything mutable or use loads of parameters and unclear functions.
For your playground the fix is to change a
and b
to return i32
rather than &i32
. Or change a
so it doesn't mutably borrow self.
Perhaps you can create an example that is closer to what you're really trying to do.
It is difficult to describe the real problem. it is just that i cannot use fields mutable AND immutable. Which makes writing centralized code difficult or impossible.
It is incredibly frustrating. The separate structs all work perfectly but it seems impossible to "gather" functionality inside one superstruct.
struct Context
{
store = immutable
renderer = mutable but needs store
}
impl Context
{
fn dothis() {}
fn dothat() {}
fn render() {}
fn getinfo() {}
}
This is often tough in Rust! If you figure out a nice, generally usable solution, let me know!
One straightforward option in the example you've given is to put the immutable shared struct in an Rc (or Arc if you're using threads) and cloning that to every structure that uses it.
You can use RefCell (or Mutex with threads) in such a shared struct if you need mutability, but they gets messy quick, so try to avoid it.
Cloning or moving data instead of mutating can be a good option, for example returning a new state instead of mutating the existing state, so you can have a single immutable shared reference while you're doing all your logic, and then the actual update can happen at the top level.
Sometimes there's a library that solves this for you in a fancier way using an internally complicated way, for example the ECS design used by eg. bevy.
If you can, still do try to design your program to avoid needing these, though it's often not clear how that could work.
The feature for gathering is modules not structures.
In terms of data you try to keep it separate. So store and render might be in separate variables.
If your creating a multi-use structure where code only needs a subsection; you represent the subsections using traits. You have to ensure your not trying to grab different subsections at the same time.
If this isn't too much trouble, could you please point to some code that uses this technique or give a brief example? This sounds useful but I'm not sure exactly what you mean.
Rust allows multiple borrows (mut and non-mut) from Context when you pass its fields as parameters to the functions doing the work. This avoids holding multiple borrows from Context self
. However, it means breaking up functions in a particular way for the situation and accessing Context
fields by name.
Here is an example that is probably not at all what you really want, but I'm hoping you can see the general idea. I used Renderer::render
as the method that splits the borrows and has the higher level logic. But you could do this in some other place (even in a Context
method although that's not as clean to me).
struct Store {
state: i32,
}
struct Renderer {
state: i32,
}
struct Context {
store: Store,
renderer: Renderer,
}
impl Renderer {
fn dothis(&mut self, store: &Store) {
// ...
}
fn dothat(&mut self, store: &Store) {
// ...
}
fn render(context: &mut Context) {
context.renderer.dothis(&mut context.store);
context.renderer.dothat(&mut context.store);
// ...
}
}
The "trick" is that render
is accessing the fields of Context
by name, not using a getter method on Context
. This allows Rust to see that the borrows are disjoint.
If getter methods on Context
were used like below, this would borrow self
for Context
multiple times (some mut and some non-mut), and then you'd have the original problem again.
fn render(context: &mut Context) {
context.get_renderer().dothis(context.get_store());
context.get_renderer().dothat(context.get_store());
// ...
}
Indeed I can avoid the problem, but the deeper you get into a hierarchy the more parameters you need, with unreadable and slow code as a result. I don't want my game character to have an update function with 4 parameters up the hierachrcy.
I will try and create a short example later on...
But the internet is full of the same question. Without a clearcut strategy as far as I can see. For Rust you probably have to think differently until you "get it"?.
You describe the problem perfectly. And what I want is mutable access to a struct with immutable access to its field, which is impossible. Or the compiler refuses to compile a little helper function because it (the borrow checker) only looks at the parameters.
In other languages you can protect your data and write clear code.
But most probably that it is my (in)experience...
I did not give up yet, because of the insane performance and low level clarity.
I had a similar case, saw a 2-piece example of classes and made my own 3-piece one.
The 2-piece pattern, let's call it Data&Calculator:
// this is immutable
struct Data {
data: HashMap<ItemKey, DataItem>, // or something more complex
}
struct Calculator {
heap: BinaryHeap<(usize, ItemKey)>, // rank & item's key
}
impl Calculator {
fn new(data: &Data) -> Self { ... }
fn do_job(&mut self, data: &Data, input_params...) -> ... { ... }
}
In this example, you keep Data
in shared memory, as it's immutable, and Calculator
can live in a thread. But sometimes you have to pass it &Data
to look it up.
In my case, I had to carry 2 or even 3 objects instead of one Data
, and it made sense to store them somewhere together, at least as refs.
So this is what I call computer
pattern: there's ROM (or HDD), there's CPU (like calculator in the previous example), and RAM (where you keep heap of the previous example).
struct ROM {
data: HashMap<ItemKey, DataItem>, // or something more complex
}
impl ROM {
fn fill_ram(&self) -> RAM { ... }
}
struct RAM {
heap: BinaryHeap<(usize, ItemKey)>, // rank & item's key
}
struct CPU<'a> {
rom: &'a ROM,
ram: &'a mut RAM,
}
impl<'a> CPU<'a> { // all calculation methods go here, where we have both mut & immut structs
fn new(data: &'a ROM, ram: &'a mut RAM) -> Self { ... }
fn do_job(&mut self input_params...) -> ... { ... } // don't need to pass Data anymore
}
The benefit here is that you both have everything together in CPU, but even though you may need to drop it (like when you need to modify Data
), recreating it costs nothing.
I'm confused, at this point.
But every single damn tutorial that I read that is teaching how to write large programs teaches you not to do that. You shouldn't use god objects and other such teachings.
Now we have language which makes making this mistake hard… why is that an issue?
You should have learned to avoid that, anyway. Rust just makes something that is usually you reviewers prevent and stops that at the language level.
You have missed “not” somewhere. Maybe this: in other languages you can NOT protect your data (when you write what you perceive as clear code).
Every single game that I saw had hidden couplings between objects that either took years to fix or were just kept unfixed because it's too hard.
Why not? Either they need that data and then I want to see that they are accesing that data or, perhaps, they don't actually need that data and you need to structure your program differently.
What causes you frustration when you program in Rust is not the fact that there are so many connections between components, but because Rust just simply exposes your lie: you can use RefCell or Mutex to be able to use shared mutable data, or have an update function with 4 parameters up the hierachrcy or do some other trick… but no matter what you would pick your data looks like a spaghetty of pointers — but that's simply how at was always represented in your programs! Other languages just simply made it possible to pretend that there are some clean separation between concepts while Rust exposes that lie.
You can always put every object into Rc<RefCell>
or Arc<Mutex>
and pretend that you are programming in Java or Python. Yes, it would expose something that most other languages desperately try to hide, but IMNSHO that's not a bad thing. Pointers are the same thing as goto, ultimately, and while Rust doesn't eliminate them it exposes them for what they are.
They cause the exact same problems in Rust as in other languages, the only difference is that in Rus you can see where these problems come from.
Snd Rust actually gives you two choices, on of them “opt out”:
- Either structure your program code in a way that it would be actually clean (and not clean via clever replacement of
goto
with pointers). - Or embrace
Rc<RefCell>
orArc<Mutex>
and accept the fact that your data structures are a mess and were always a mess.
I wonder why people are desperately trying to seek some third way which would allow them to continue to pretend that their data is clearly organized and nicely separated in layers. That was never true even when you programmed in C# or Swift, why the exact same approach in Rust make you cringe?
Yes... I know, I saw it. No God objects.... Your answer makes sense to me. And If I can avoid references, Arcs, RefCell, Rc etc. I will do so definitely and if possible even lifetimes.
Rust exposes the lie...
There seems to be some truth in that
But not completely. Sometimes you absolutely know what you are doing but the compiler just cannot check that.
What I meant with "protect" is writing a class which internally mutates itself but on the outside there is some readonly function or property. Please forgive me: 20 years of Delphi and C#...
I wonder why people are desperately trying to seek some third way
Because they have that kind of experience.
Why not? Either they need that data and then I want to see that they are accesing that data
That is the problem!! The impossibility to pass 4 parameters as one ref to a struct with 2 fields immutable and 2 mutable etc. etc.. Not a God struct.
I will continue trying to get it to work 'rusty'.
BTW: I will checkout the ROM/RAM/CPU example.
Thanks
So this seems the solution I was looking for... thanks to the CPU example.
Instead of 4 or 5 params one compound clear param.
pub struct Ctx<'a>
{
pub config: &'a Config,
pub store: &'a Store,
pub renderer: &'a mut Renderer,
pub blitter: &'a Blitter,
pub current_level: &'a Level,
}
impl Program
{
pub fn run(&mut self)
{
// create here
let config: Config = Config::from_file();
let store = Store::open(&config.data_directory);
let mut renderer = Renderer::new();
let blitter = Blitter::new();
let level = Level::default();
// ref here
let mut ctx = Ctx // edited to mut
{
config: &config,
store: &store,
renderer: &mut renderer,
blitter: &blitter,
current_level: &level,
};
// pass down as param down the hierarchy
GameScreen::run(&mut ctx); // edited to mut
}
}
I don't understand, that's the same thing you had earlier. If you borrow multiple times from Ctx you'll get the same errors.
Borrowing from Ctx
is fine because its fields are disjoint. There is only one &mut
field. In fact, this pattern is one of the solutions Niko provides for the problem in After NLL: Interprocedural conflicts; View structs as a general, but extreme solution.
That's not all that different (although it helps) than what I suggested earlier, which they rejected. That led me to believe that they wanted to pass Ctx down to nested functions, mixing mut and non-mut access along the way, which will run into the same problems fairly quickly. But there's nothing wrong with trying it.
The view struct approach is something I would only use as a very short lived struct, and I would construct it for one operation at a time, not for the duration of the program. Rust doesn't work well with long lived references. But perhaps if there is only one thread, for this situation it will work.
Well, I can't say why your suggestion was rejected, but it is feasible to mix shared and exclusive access using the view struct, including with methods on the view struct arbitrarily calling each other. This is exactly what Niko describes. It is allowed because &mut &T
is never mutable.
You are right that views are meant to be temporary, as indicated by their lifetime parameter. But they are inexpensive to create, so the burden is really only boilerplate.
You're right, it works as long as no nested method returns a reference, since that would borrow Ctx. That was the sort of thing done in the OP, but who knows if that's what they'll really be doing. It would be good to know more.
Absolutely. I was reacting to their use of Ctx in Program::run
where it looks like it lives for the program duration.
Yes, I cover using Arc and friends immediately below that quote, but while it's generally usable it's not exactly nice. So far, view structs are the closest I've seen to getting both (and it's nice to have a name for the pattern), but they're a pretty heavy hammer.
I wonder why people are desperately trying to seek some third way which would allow them to continue to pretend that their data is clearly organized and nicely separated in layers. That was never true even when you programmed in C# or Swift, why the exact same approach in Rust make you cringe?
Because a language isn't just responsible for expressing the desired behavior, but also for helping us find that desired behavior. Rust is great at the former, but quite bad at the latter: here because doing standard practice to modularize some existing code causes everything to break despite it being logically identical, an issue unique to Rust. This isn't to say Rust made a mistake here, but there's no reason to be confused by people looking for ways to do things better.
pub struct Ctx<'a> { pub config: &'a Config, pub store: &'a Store, pub renderer: &'a mut Renderer, pub blitter: &'a Blitter, pub current_level: &'a Level, }
If only renderer
is mutable, why not separate it from the rest and use it as the central object, and pass the Ctx as a read-only param. Passing one param is certainly not a burden. Separating the mutable and immutable data is a clean design and works well with Rust. Then there is no need for a view struct as a workaround for borrowing issues.
pub struct Ctx {
pub config: Config,
pub store: Store,
pub blitter: Blitter,
pub current_level: Level,
}
impl Renderer {
fn dothis(&mut self, ctx: &Ctx) {
// ...
}
fn dothat(&mut self, ctx: &Ctx) {
// ...
}
}
I don't understand, that's the same thing you had earlier. If you borrow multiple times from Ctx you'll get the same errors.
I don't! (btw: i edited the mut in my example)
Inside 'GameScreen' and further down in 'Game' there is:
impl Game
{
fn setup(&mut self, ctx: &mut Ctx)
{
// cache or get from cache (load on demand) and return vec
self.tiles = ctx.renderer.prepare_tiles(ctx.store, ctx.current_level);
}
}
If only
renderer
is mutable, why not separate it from the rest and use it as the central object, and pass the Ctx as a read-only param.
Definitely a good option. I will experiment with that as well.
There is one more 'catch' to this all: If at some moment in time we need one extra tiny detail inside the ctx "view struct" we do not have to change all signatures of all functions dependant on it.
What exactly is the problem Ctx living for the duration of the program?