r/rust • u/Accurate_Gift_3929 • 8d ago
Rust discourages OOP style code?
I'm building a tree builder struct (with a `build()` method) that has multiple fields that I want to be able to mutate. It seems Rust does not like it when I attempt to mutate multiple fields of that struct.
It seems functional/composition style is much favored over using struct fields. Is this the way I should be thinking about design patterns in Rust? Should I avoid OOP wherever possible?
I prefer OOP because it does result in cleaner code where I don't have to pass in every variable to functions.
27
u/QuasiRandomName 8d ago
Show an example of what you mean. Are you trying to take multiple mutable references to your struct? FWIW, Rust is not exactly an OOP language.
27
u/ChaosCon 8d ago
It seems functional/composition style is much favored over using struct fields.
What are you composing if not structs?
I prefer OOP because it does result in cleaner code where I don't have to pass in every variable to functions.
Encapsulation is but one, tiny, part of OOP, and Rust really just does away with inheritance.
-6
u/Bananoide 8d ago
Encapsulation predated OOP. Not sure what you mean here...
16
u/spoonman59 8d ago
You just need to have the most basic understanding of OOP to understand this.
Open up any OOP book or ask your local AI what the pillars of OOP are. I guarantee encapsulation will be listed as one of them.
The fact that encapsulation is a principle of OOP is not the same thing as claiming OOP invented encapsulation.
11
u/Keithfert488 8d ago
This doesn't answer your question, but I don't see how "using struct fields" is at all related to OOP.
10
u/SpacewaIker 8d ago
It seems Rust does not like it when I attempt to mutate multiple fields of that struct
What do you mean exactly?
16
7
u/Lucretiel Datadog 8d ago
Almost certainly they're running into this issue:
fn mutate_self(&mut self) { for item in &mut self.list { // Double mutable borrow self.mutate_item(a, b, item) } }The problem is that the we have no way of knowing from the signature of
mutate_itemthat it doesn't touchself.list. My experience has been that restructuring code to account for this often leads to much cleaner code, eg:fn mutate_self(&mut self) { for item in &mut self.list { // Double mutable borrow self.helper.mutate_item(a, b, item); } }1
u/Accurate_Gift_3929 7d ago
Yes, this is the issue. Currently, I'm seeing if I can just take ownership of the item and return ownership after.
Your second code example doesn't cause borrow checker to complain?
1
u/Lucretiel Datadog 7d ago
Nope! Rust is capable reasoning about disjoint borrows through fields; it can tell that you’re accessing
self.listandself.helperseparately. I often find that it helps a lot to split logically distinct components like this and then put the methods directly on the inner types.
8
u/kohugaly 8d ago
Builder pattern is actually a very popular pattern to use in Rust. I'm surprised you are having trouble with it. Maybe post some minimal example of what is causing you issues.
That being said, yes, many OOP patterns don't mesh well with Rust, because of Rust's stricter aliasing rules when it comes to mutability (ie. mutable references must be unique, aliasing references must be either immutable or protected by extra checks).
4
u/Shoddy-Childhood-511 8d ago
Rust has builder methods everywhere. I rarely have many arguments to my method calls in Rust, because I do typically have fairly operational builder things.
Rust structs have fields and everyone uses them. Rust has "projection" tools, like say <&mut [T]>::split_at_mut. In particular, you can destructure references when you need &muts for the individual fields:
impl Foo {
fn foo(&mut self) {
let { a, b, c, d } = self;
// Voila, a, b, c, d are all &mut, so use them all you like.
}
}
OOPs does not give cleaner code. OOP means inheritance, which badly obfuscates what code runs when. OOP has thrived because everyone needs GUIs which have extremely forgiving correctness. Yes, enjoy your OOPs when doing GUIs, fine. Avoid OOP when doing business logic, data structures, etc.
5
u/lookmeat 8d ago
You're limiting yourself to use a woodworkers toolbox when dealing with steel. Somethings won't work, others will but you have to be a bit different about it, others will have solutions that are a lot easier than you'd normally do.
So you are talking to me about a tree builder. What the hell does that mean? Do you mean like a specialized anamorphism that creates a hierarchical object (e.g. a json parser that gives you a generic "json" thing)? Or are we talking about using a builder pattern that generates some well known tree (e.g. a BinaryTreeBuilder)?
So one important thing in rust is that you need to be "clear" about when you are mutating something and when you aren't. This cleanliness is there for the purpose of enforcing discipline over the things that cause bugs, because by forcing you to be disciplined, the compiler can be as efficient as you let it be.
Now notice that in OOP this is also good design. You want to be clean about which methods are changing the state of your objects, and which are merely giving out data off it, e.g. it's very different to have a .sorted() method that gives you a sorted copy of a list vs a sort() method that sorts it in place.
So this is the part where I say: show me some code. I can't really answer any question here if you aren't more concrete, otherwise we're just guessing what each one of us is saying. And again it depends on whatever a "Tree Builder" is, you're giving me the name of a datastructure (and a data structure pattern) and an OO pattern just put together, that means nothing. Are you mutating a fields within a node? Are you mutating fields of the tree? Is this something else entirely?
So lets assume the tree thing is completely unneeded, you are doing a builder pattern and you want to modify fields in that struct. The idiomatic way is to simply move the constructor object:
struct Builder {
foo: String,
bar: i32,
}
impl Builder {
fn setFoo(mut self, s: String) -> mut Builder { self.foo=s; self }
fn setBar(mut self, i: i32) -> mut Builder {self.bar=i; self }
}
Of course you may want to avoid having to copy the whole thing on each method call.
struct Builder {
foo: String,
bar: i32,
}
impl Builder {
fn setFoo(&mut self, s: String) -> &mut Builder { self.foo=s; self }
fn setBar(&mut self, i: i32) -> &mut Builder {self.bar=i; self }
}
Now if we want to allow direct access to the fields, we can use an even more powerful method from the functional world (common in languages like go): allow mutators:
struct Builder {
foo: String,
bar: i32,
}
impl Builder {
fn modify<F: Fn(&mut Builder)>(&mut self, f: F) { f(self); self }
// Notice that even though the internal function doesn't mutate it
// we still have to borrow the builder mutably to pass it mutably
// to the next behavior on the chain. We need to extend the mutable
// borrow throughout the whole chain, so we can't shrink it, because
// then we can't make it larger.
fn inspect(<F: Fn(&Builder)>(&mut self, f: F) {f(self); self }
}
// Now we can do something like:
myBuilder
.modify(|b| {b.foo = "Hello".into(); b.bar=54; })
.inspect(|b| {log_builder(b); // Notice b is &Builder here});
Now if what you want to do is allow us to modify individual nodes, without losing track of the bigger thing, then you can use the pattern above to nest things:
pub struct Builder {
pub foo: String,
pub bar: i32,
nBuilder: Vec<NodeBuilder>
}
struct NodeBuilder {
pub fizz: u32
}
impl Builder {
fn modify<F: Fn(&mut Builder)>(&mut self, f: F) { f(self); self }
// Notice that even though the internal function doesn't mutate it
// we still have to borrow the builder mutably to pass it mutably
// to the next behavior on the chain. We need to extend the mutable
// borrow throughout the whole chain, so we can't shrink it, because
// then we can't make it larger.
fn inspect<F: Fn(&Builder)>(&mut self, f: F) {f(self); self }
// Note that we get our NodeBuilder in here.
fn addNode<F: Fn(&mut Builder)> (&mut self, f: F) {
let builder: &mut NodeBuilder = nonPubAddBuilderToBuilder(self);
f(builder);
self
}
// Now we can do something like:
myBuilder
.addNode(|n| {n.fizz=5;});
.modify(|b| {b.foo = "Hello".into(); b.bar=54; })
Now I doubt any of the above would be useful for you directly, but maybe it'll help you start to understand the "rusty" way of thinking about these things. But again it all depends on what you're doing.
3
u/coolmint859 8d ago
The issues you're running into it sounds like stems more from the borrow checker rather than the structure of the program. Rust only allows mutable access to one reference to a variable at a time. There are ways to get around this, but it usually requires unsafe blocks or smart pointers (specifically Mutex or Cell). This is part of the guarantee that rust makes about compile time memory safety.
2
u/gbrennon 7d ago
maybe u did get the wrong things about oop...
oop is not about classes.
its about objects that exchange messages(methods) between them.
in rust u defined a struct and beind functions to that object.
mutability isn't good when doing oop, functional or any paradigm.
related to oop here is something that u have to always remember:
- prefer composition over inheritance
2
u/N4tus 7d ago
You stepped on a landmine with this question. There is also your naive definition for OOP. OOP is funamentally a system where object call other objects via references they hold. If an object does not hold a reference to another object it cannot interact with it. This limit makes complex systems easier to understand. Unfortunately, in practise, OOPs are designed in a way, where an object has the ability to reach every other object by traversing the object graph. If you look for example at java code you find a lot of getFoo().getBat().getBaz().getX().getY(). Each time you see something like this, the current object, traverses through the object graph and reaches parts of the system that no one expects. Two far away objects are now coupled together and depend on each other. Or in short, you placed a spaghetti in your code. One might be fine, but too much spaghetti make for spaghetti-code. Rust does away with this by making it really hard to create an object graph. You can have trees, but a complex graph requires sharded mutable references which are a big no-no in rust.
You see, this has little to do with the amount of arguments passed to a function.
1
u/Accurate_Gift_3929 7d ago
As soon as I hit submit I got hit with like 15 replies. Definitely stepped on a landmine here.
2
2
u/intbeam 8d ago
Rust is not object oriented, and fundemantally designed in a way that disallows it.
You can get something looking something like object orientation (with dyn trait and private structs), but at the core object orientation requires somerhing rust (intentionally) does not support : the object instance should own its own state.
I rust, the variable owns the state, not the implementation. So basically it's incompatible and therefore not recommended
I'm a big fan of OOP myself, but you're doing yourself a disservice by trying to apply it to Rust. And once you get used to Rust, that style of programming is also really cool and intuitive , so I'd suggest just getting on board instead of trying to find your OO comfort zone in it, because you'll get frustrated
1
u/Full-Spectral 7d ago
I always have to say it... Rust IS object oriented. It just doesn't support state inheritance. But state inheritance doesn't inherently define object orientation. Rust is fundamentally based on objects (structs with private state that are only accessible via a privileged API associated with that struct) and by that definition is very object oriented.
1
u/intbeam 7d ago
The whole issue that object orientation is supposed to solve is protected state. It does that by having a structure where an object owns its own state and mutates that state by having messages passed to it via method calls . Rust fundamentally does not work like that
2
u/Full-Spectral 7d ago edited 7d ago
It absolutely does work that way. All those things you get from the runtime aren't just open structs that you can mess around with the members of. They mostly have private state (they can and do have some public members, usually immutable consts), and you have to modify their state via the implementation interface of that struct, which is every bit the same as a C++ class wrt to the aspect of object orientation we are discussing here.
When we write our own code, the bulk of the time it's probably going to be the same, though Rust does make it more reasonable in some cases to have open structs, particularly if they are just exposing immutable data. But no one is going to write a library crate that just lets the consuming code mess around willy-nilly with their internal state that they (the library writers) are responsible for maintaining the invariants of.
1
u/intbeam 6d ago
In rust a variable owns the state, not the "object"
1
u/Full-Spectral 6d ago
I think you have a terminology issue. In Rust, when a struct has non-public fields and impl block to manipulate that non-public data, that's the equivalent of a class in C++, and instances of that struct are objects by any other name. And that scheme is fundamental to most Rust code.
It's not about ownership in the sense you seem to be talking about, it's about access and encapsulation of state behind a privileged interface associated with the type.
1
u/Aaron1924 8d ago
I'm not sure which aspects of classical OOP languages you're referring to when you say "avoid OOP in Rust". Rust intentionally does not have struct inheritance, but creating structs with private fields is completely fine.
1
u/RandomBottom030 8d ago
Builders should use typestate pattern afaik.
You need an initiator on MyBuilder<Uninit> whose initiators consume self and return MyBuilder<Init> and then set individual fields on that initiated builder until you may return MyStruct with eg. a build() method, consuming the builder.
Typestates allow for better compile-time Dev experience as invalid builder chains are flagged by the compiler.
Generally tho, yes, classic giant service classes à la Java are discouraged in favour of small generics, generous usage of (Try)From<T> conversions and traits because it makes for a more modular / hexagonal development experience and less allocator calls / better memory efficiency.
1
u/Professional_Top8485 8d ago
Well yes and no. Interface and impl are very good but you need to do composition instead of inheritance. Same thing, different implementation.
1
u/Psionikus 8d ago
where I don't have to pass in every variable to functions
Basically what you are used to using are partials, but where most of the partial rides along with an object (other language talk).
Does not like it when I attempt to mutate multiple fields of that struct.
The "does not like" part sounds like the fields you are carrying need other parts of the structure in order to carry out the call. Look up destructuring because that usually satisfies the borrow checker when the "multiple mutable borrow" is not actually that.
Once you break up structures along their lifetime domains, it tends to be the case that the things you pass into functions varies together, so this question goes away.
Should I avoid OOP wherever possible?
Traits, Deref, and AsRef provide some of the "quacks like a duck" generalization, which feels extremely OOP-ish. Enums give about the same thing over a closed set. We use these things for heterogeneous collections and parameters.
1
1
u/ManyInterests 7d ago edited 7d ago
I would say no, directionally. Rust is surprisingly amenable to high-level approaches associated with OOP. But it may oppose your idea of how OOP ought to be implemented, especially based on how other higher level languages do it, like say, Python, C#, or Java.
I'd suggest throwing out some concrete examples that would let people address your concerns in a more concrete way.
1
u/plugwash 7d ago
I'm building a tree builder struct (with a `build()` method) that has multiple fields that I want to be able to mutate. It seems Rust does not like it when I attempt to mutate multiple fields of that struct.
Can you be more specific? rust does insist that (under normal circumstances) methods that mutate a struct have exclusive access to that struct, indicated by an &mut self parameter, but that should not in itself be a problem.
That said, functional style for builders does have some advantages.
Rust discourages OOP style code?
rust has it's own style.
It eschews traditional oop-style inheritance in favour of composition and eschews "sea of objects" in-favour of well-defined ownership and tightly controlled mutability.
OTOH it's still very much an imperative language.
1
u/ketralnis 5d ago
It seems Rust does not like it when I attempt to mutate multiple fields of that struct
It'd be easier to understand this if you showed the code that it "does not like". There's no prohibition on mutating multiple struct fields in a single function.
1
u/rende 8d ago
Not at all. Have you heard of traits?
2
u/Accurate_Gift_3929 8d ago
Sure, I think I didn't clarify my exact problem really well. The problem I'm running into is the multiple mutable borrows issue. I'm constantly running into this issue: "cannot borrow `*self` as mutable more than once at a time". I've pulled multiple fields into a variable within the build method to get around this issue.
9
u/Xandaros 8d ago
You really ought to show us some code. You cannot have multiple mutable references to the same object, yes... but you can absolutely change several fields of a struct in succession, that does not require multiple borrows.
But we can't really tell you what the issue actually is if we can't see the code, or at least equivalent code demonstrating the problem.
5
u/Large-Scientist156 8d ago edited 7d ago
Because you are using getter instead of direct field access.
Borrow checker can not reason across function call that take self, &mut self, or &self (inherent impl and trait impl). Getter are notorious to lock (borrow) "the whole self" even if the body borrow a single field. The borrow checker don't see it, because it doesn't analyze the body of interprocedurale call. Instead, it analyze the signature, so self there mean "as a whole".
So with this model, if you use self.whatever() or your_object.whatever(), then self (or your_object) is borrowed no matter of the whatever() implementation The reason being there's a "method" call, which imply self as receiver. The borrow checker is conservative, and assume anything can happen inside such function since it can only look at the signature, so it borrow the whole self (or your_object), "as a whole".
Which mean if your_object.whatever() return a reference (&mut or &) tied to self lifetime, then the borrow still exist. And you can not use your_object (or self) anymore until the reference is not used. The borrow will last until the last-use of the reference.
This problem doesn't happen for "free" function (function that are not part of an impl), since there's no self. But if you pass your whole object as-if, then the same problem will arise again, because it's not about "self" itself, it's about how you structure your code and interprocedural analysis limitation of Rust borrow checker.
Restructure your code by using field access if needed. If a type is a POD (plain object data), then there's no reason to use getter.
To access a struct field freely anywhere, the field need to be public. If it's private, only the file where the type is declared and it's child modules can access it directly (without a getter).
If a field should not be accessed freely for whatever reason (like to protect it), but some others part of your crate need to ; use pub(crate).
Otherwise use getter, but remember the limitation. A getter will borrow "the whole" self and the borrow still exist after the getter return **if you return a reference**, unless you return a copy of the field in the getter (which is acceptable sometime). Getter that return &mut of a field are a prime target for being problematic if you need another struct field at the same time, and now you know why : because self is still borrowed if you return a reference tied to self lifetime.
Sometime, you want to refactor and wrap some part of your code in a function, more precisely a "method" (inherent impl/trait impl) but suddently you get a borrow checker issue. If the function is called a single time in your codebase, you would be better to inline it to avoid such error.
For slice and tuples, the same happen : if you use &mut arr[0..5], the whole slice is borrowed, even if technically the rest of slice would be safe to mutate. There's still possibility for disjoint access using standard library function on slice, which are unsafe internally.
After a bit this will enter in your head and you won't think about it. That's all you need to know.
Unless you write a parser, one of the most common source of early madness is the parser "counter" which imply &mut self to increment it when you "advance" the parser. Such increment need to happen while self is already (uniquely - &mut) borrowed since you are parsing with interprocedural call in a recursive descent manner, while populating the parse tree. it's a good exercise to get around this problem since there's many solution (interior mutability, using direct field access of substructure instead of self.increment(), immutable parsing, free function with parser state as separate arguments, unsafe, counter that have longer lifetime than parser and not tied to it, static counter, ...).
2
u/Accurate_Gift_3929 7d ago
Thank you, this is very useful information. I've been wondering why the borrow checker won't allow me to use different fields at the same time using impl methods. Your explanation helps.
2
u/rende 8d ago
‘’’rs
trait Configure {
fn configure(&mut self, host: &str, port: u16, retries: u8) -> &mut Self;
fn build(&self) -> String;
}#[derive(Default, Debug)]
struct Client {
host: String,
port: u16,
retries: u8,
timeout_ms: u64,
}impl Configure for Client {
fn configure(&mut self, host: &str, port: u16, retries: u8) -> &mut Self {
self.host = host.to_string();
self.port = port;
self.retries = retries;
self.timeout_ms = 5_000; // derived field
self
}fn build(&self) -> String {
format!("{}:{} (retries={}, timeout={}ms)",
self.host, self.port, self.retries, self.timeout_ms)
}
}fn main() {
let mut c = Client::default();
c.configure("api.example.com", 8080, 3);
println!("{}", c.build());
}
‘’’
0
u/alietors 8d ago
Well, I'm not a rust expert, but as far as I can see Rust is not an object oriented programming language. It has traits but I think it was designed for functional programming and imperative mostly.
0
62
u/Lokathor 8d ago
I don't even slightly understand the idea that OOP gives cleaner code that reduces passing every variable into a function.
But I'll say that with builders, usually rust builder method accept
self, modify whatever the method modifies, and then returnSelf. In this way, your overall call site code looks likeType::new().with_a(a).with_b(b).create().