Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I mean it depends. Many of aspects of Rust that are perceived as sharp edges are in fact the programmer bringing in their preferences and paradigms from other languages and trying to program that way in Rust.

I was one of those and tried to do OOP in Rust. It was a pain. At some point I gave up and was like: "Okay Rust, I do it your way, I just want this to work". And it worked flawlessly and easy. I literally had to overcome my ideas of beauty in code to realize how Rust is meant to be programmed in (more data oriented, less object oriented).

This was a really good lesson for me, as I had to question similar aesthetically motivated decisions I made in other languages in the past.



Rust actually supports most OOP features, with the main exception of implementation inheritance. And implementation inheritance is a nasty footgun in large-scale software systems (search around for: "fragile base class problem"), in a way that just doesn't apply to simple composition and pure interfaces (traits). So it's hard to fault Rust for including the latter and not the former.


For me it was the web-of-pointers strategy I had to unlearn. A child object keeping a pointer to its parent is misery in Rust. It forces you to either make the child completely independent or really prove the parent will be around until the child disappears.

90% of the time this is dumb overhead, but 10% of the time it found a bug in some edge case, so I learned to appreciate it as a tough teacher, and my designs got better for it.


We solved this with flat vectors and just sharing index values in cheap walker objects. It is much nicer to work with compared to arc/weak pointers.

Code here: https://github.com/prisma/prisma-engines/tree/main/libs%2Fda...


How is this fundamentally different from raw pointers?


Not OP

It serializes better, it's memory safe, it can be much faster in performance terms, you can get better memory usage if you're holding a lot of "pointers" because the indexes don't need to be 64 bits.


But you lose all ownership tracking / safety that Rust is famous for.

I just can't help but wonder what it means for that paradigm, when the most popular answer to "how do I model my entities in safe Rust" is basically "backdoor the safety".


Which way did you go, (1) not use pointers from child to parent (functional programming approach), or (2) kept using such pointers but made it work? Your first paragraph sounds like (1), but the second sounds like (2).

If it was (2), it sounds like you made it work without unsafe code? Can you share some code?


> designs got better for it.

What is better about them?


>"90% of the time this is dumb overhead, but 10% of the time it found a bug in some edge case, so I learned to appreciate it as a tough teacher, and my designs got better for it."

Sounds kind of like Stockholm syndrome


And you sound like you are part of the reason why we still have exploitable null-pointer bugs in 2022. Imagine a structural engineer that would say "all that static and dynamic analysis is dumb overhead 90% of the time, so I am going to skip it". Imagine an electrical engineer who would go like "all these wire gauge calculations are exhausting, 90% of the time my installations don't catch fire"

Seriously, I sometimes wonder what is wrong with the whole field of software development.


Nothing is wrong. Same shit as in any other area. It is a usual trade-off between what you pay and what you get in return, some of it highly politicized.


> Rust actually supports most OOP features, with the main exception of implementation inheritance.

I've tried some basic OOP.

Fields from the base class need to be redefined in each child, and making shared methods private require an ugly workaround. Both concepts are very basic OOP concepts, not advanced or inherently/tendentially dangerous, and the results is that even basic Rust OOP requires a lot of boilerplate. It's workaroundable with macros if one wants, although they have some limitations, and I guess this is the reason why the macro approach is not widespread.

Some devs use composition to emulate it, but the result is another form of boilerplate (very noisy builder patterns).

Regarding "non-basic" OOP features, I remember that Rust GUI framework programmers uniformly complain about Rust not being appropriate to translate the OOP hierarchies typically used in GUI frameworks, so the gap is significant (this is not inherently bad; not supporting OOP can be a respectable choice, but matter of factually, this does create a gap in some cases).


You can define a field with the type of the base class in the subclass, and impl Deref for SubClass, with Target = BaseClass.


You can, but the docs for Deref beg you not to use Deref for types that aren’t smart pointers because of how confusing and surprising the results can be to an unfamiliar reader of the code.


Rust itself does not have much support for delegation (the building block for the features you're referring to) since it's pretty much syntactic sugar anyway, but you can use crates like 'ambassador' to provide that.


The idea of object inheritance is silly. If you need a 'base' class A for data type B just do

    struct B {
      a: A
    }


Does OOP require inheritance?


Kind of, it needs the ability to express polymorphism and extend concepts.

Now how that is done is practice, can be done in various ways.

Class inheritance, which is what many think of.

Interface inheritance or conformance, which is available in many languages via traits, patterns, data classes, functors, or other names, and OS ABIs like COM.

Prototype inheritance, clone a instance and monkey patch in place (like SELF)

Via composition and delegation for not handled messages.

Then many languages offer a combination of those approaches.


For me, one of the main reasons to use OOP is dynamic dispatch / runtime polymorphism. I have not spent much time with Rust, but it seems like that is a bit cumbersome there, and doesn't exactly work like you would expect from Java, C++, Python, ....

I mostly use OOP when I have a bunch of Foos and Bars, and want to treat them differently in some places. Instead of having ifs in each function, I use inheritance, and override the methods that I want to treat differently. It's mostly about heterogenous containers and avoiding explicit ifs. When you use it that way, it's safe and convenient and I've never ran into the fragile base class problem or other typical issues.


It definitely works differently to C++/Java/Python, but IMO this is one place where Rust is a huge improvement. You are effectively always generic over an interface (trait in Rust) rather than a base class, and this is much more flexible as a given strict can implement as many interfaces as it likes rather than being stuck in one class hierarchy (or having to deal with the pain of multiple inheritance)


It is no different than using interfaces, protocols, pure virtual base classes....


It's similar, but there are some differences. Notably, my package can implement my trait for your type, while my package (usually? always?) cannot make your type inherit from my virtual base class.


Depends on the flexibitly of the language, there isn't a golden way to do that across all variations of OOP.


Wrapper classes should be equally good once Valhalla (jdk) lands, without introducing another pointer indirection.


Interesting but Java gets more and more complex in a way that does not make me want to write things in Java.


Not a Rust expert but: doesn't Rust support this use-case via Box<dyn SomeTrait> , and leveraging default Trait methods, only overriding the defaults you want to for each type implementing the Trait?


What you describe can be achieved in Java via composition (instead of inheritance) and using interfaces.


Does it?

I've seen it being repeated ad nauseam without any concrete backing.

I mean it has some OOP concepts. But it's mostly in traits. Saying Rust is OOP is a bit like saying Chimera is a Goat.

Anyone that tried to use OOP in Rust, knows it's next to impossible.


Yes it does, OOP is a spectrum not "like Java does it".


I'd consider something OOP if it has the same expressive power i.e. things expressible in OOP languages are easily expressible in another OOP language as well. And for Rust that's not the case.

Otherwise you end up with Haskell is an OOP language.


I will debate it is one, yes.

Just like being an FP language isn't "like Haskell does it", when I learned FP, Haskell didn't even exist.

Try to express Eiffel, CLOS, SELF or BETA in Java.

By the way, here is One Weekend Raytracing in Rust, perfectly using OOP with dynamic dispatch and interfaces (sorry traits).

https://github.com/pjmlp/RaytracingWeekend-Rust


Ok. But noticed what I said. *Things* that are easy in one OOP should be similarily easy to express in other OOP language.

Counter example: Html5 DOM.


Counter example of what?

A graph representation of nodes based on JavaScript object model?

Fine, https://rustwasm.github.io/docs/wasm-bindgen/examples/dom.ht...


Yeah but if you dig deeper you'll notice it is riddled with unsafe, casts and Deref misuse.

It's a brittle simulation of Inheritance. To transform a saying "Just because you can write Ada in C, doesn't make C Ada".


Post Scriptum: one way to prove this is that we could use https://www.youtube.com/watch?v=43XaZEn2aLc as a base.

How to prove.

Assume that we add struct inheritance to Rust (it looks similar to JS) - call it RustInh.

Does adding inheritance to Rust increase expressive power? If yes, then right now Rust can't express OOP concept as inheritance. I.e. is there a C[RustInh] = C[Rust].

And way to prove it is to look at Deref anti-patterns https://github.com/rust-unofficial/patterns/blob/main/anti_p...

I'm still not sure that fully captures my intuition of easy to write.


> I'd consider something OOP if it has the same expressive power i.e. things expressible in OOP languages are easily expressible in another OOP language as well.

That doesn't even make sense on its face. There are things which are trivially expressible in Smalltalk and I'm not sure even possible to wrangle in Java.


That's because Smalltalk OOP isn't the OOP in the sense Java is, some refer to Java Style as Class oriented Programming.


That doesn’t make your original comment any less nonsensical, and still makes no sense: Smalltalk is very much class-based. Are you mixing up Smalltalk and Self?


To me, OOP always meant passing messages to objects, but it seems everyone has their own definition depending on experience.


> fragile base class problem

I do wonder whether this is as much of a problem as people think. Most of the time it only becomes a problem when people start doing stupid things to override the parent instead of refactoring a new class out from under it.


Having studied mostly OOP/Java at the uni, and been using mostly C# at work, I realized I might actually start to find programming fun again through Go. I've studied it a bit recently and this idea of interfaces with composition over inheritance etc. made somehow a lot more sense to me, and gave me this boost to try and learn it more because it felt so enjoyable. Not even with some practical problem at hand to solve, but just on an abstract level, not having to deal with OOP classes and inheritance and all the things. Now I just need to find a Go job and get my feet a little more wet. ;P I'm talking about Go because it gives me similar feelings as Rust, but Rust I have a lot less experience with. I hope there's a similar fun and excitement to be found in Rust as well, after unlearning some old habits.


Go is fun at first, and then it becomes soul sucking. It's all boiler plate. Many large scale projects have a lack of adequate unit testing, so large code bases are particularly painful to maintain. I attribute this lack of tests due to how the code needs to be structured, you have to needlessly add 'interfaces' throughout your code to accomplish things.

Go has it's strengths, but IMO fun isn't one of them. It turns into soul-suck quickly.


I'm in the same position as OP and was wondering the same thing; finding a job using Go. Is the bloat that you see the most related to error checking? If not what else is it?


The bloat is mostly related to mocking and making a fake client for everything for unit tests, and the practices you have to follow to make that actually work.

In Python or Java, you can do something like:

class: f1(): self.some_api_call() <process api call here>

test_class extends class some_api_call(): <mock definition>

In go, you cannot do this. Fair enough, but everyone writes object-style code because it's easier and there's less boiler plate. If you do this, you can't write tests.

Instead, you have to do something like:

class: apiMethod = nil f1(): self.apiMethod() <process api call>

And assign apiMethod when you create the instance of the object; there are no constructors because they're not traditional objects, you need to create a factor method (boiler plate) or build the struct by hand (boiler plate). None of this is terribly challenging in and of itself, but if you work on large code bases, nobody did this because it's extra work, and now you have to refactor it because you want to make a change to the critical business logic and prove you're not introducing a regression (a constraint the l33t coders before you didn't have because they get to go fast and break things).

And finally, often times the thing you need to mock is in someone else's package, and it doesn't implement an interface, or the thing you need to mutate is private, etc, etc. You end up need to mock half of a package sometimes.

None of this is insurmountable, but when you do it day in and day out for years like I did, it's a real slog, and it sucks your soul out of your body. It wasn't uncommon for me to spend literally 20x as long refactoring and implementing tests as it was to ship features. If you look at some unit tests from large go projects, you'll see stuff like struct{struct{struct{struct{...}}},struct{struct{struct{...}}}} because so much stuff needs to be mocked up. And since interfaces are disjointed from classes, you won't know with certainty which methods you need to needlessly mock for your mock class until you try to compile, because the interface is defined somewhere else and not attached to a base class, it's just a definition floating out there in the source somewhere.


I'm reposting your message with the code formatted as you had entered it (the original white space use is still visible in the html source code; I've just indented it so that HN recognizes it as preformatted):

---

The bloat is mostly related to mocking and making a fake client for everything for unit tests, and the practices you have to follow to make that actually work.

In Python or Java, you can do something like:

    class:
      f1():
        self.some_api_call()
        <process api call here>

    test_class extends class
      some_api_call():
        <mock definition>
In go, you cannot do this. Fair enough, but everyone writes object-style code because it's easier and there's less boiler plate. If you do this, you can't write tests.

Instead, you have to do something like:

    class:
      apiMethod = nil
      f1():
        self.apiMethod()
        <process api call>
And assign apiMethod when you create the instance of the object; there are no constructors because they're not traditional objects, you need to create a factor method (boiler plate) or build the struct by hand (boiler plate). None of this is terribly challenging in and of itself, but if you work on large code bases, nobody did this because it's extra work, and now you have to refactor it because you want to make a change to the critical business logic and prove you're not introducing a regression (a constraint the l33t coders before you didn't have because they get to go fast and break things).

And finally, often times the thing you need to mock is in someone else's package, and it doesn't implement an interface, or the thing you need to mutate is private, etc, etc. You end up need to mock half of a package sometimes.

None of this is insurmountable, but when you do it day in and day out for years like I did, it's a real slog, and it sucks your soul out of your body. It wasn't uncommon for me to spend literally 20x as long refactoring and implementing tests as it was to ship features. If you look at some unit tests from large go projects, you'll see stuff like struct{struct{struct{struct{...}}}, struct{struct{struct{...}}}} because so much stuff needs to be mocked up. And since interfaces are disjointed from classes, you won't know with certainty which methods you need to needlessly mock for your mock class until you try to compile, because the interface is defined somewhere else and not attached to a base class, it's just a definition floating out there in the source somewhere.


for me it's lack of map/reduce


You must learn to love the boiler plate, understand why doing the boiler plate well matters.


As someone who even structures their Python in OOP, I'd appreciate elaboration on how you organize your Rust code. I've been glancing at Rust for far too long, never finding a weekend to dive in. Save me some headaches, how should I approach e.g. Customer - Product - Order relations without OOP? What is the canonical "Rust Way"?


Your customer, product and order are still going to be objects, but they won’t reference each other directly with references/pointers. Instead they’ll either store an id, or you’ll have a central registry of relationships using some kind of id (could be a key into a hashmap or an index into a vector) and then fetch data from the central store (or have the caller pass it in) at the last minute when running code that does something.


> they won’t reference each other directly with references/pointers

I'm not sure if I understood your reply correctly, but there seems to be nothing in Rust stopping one dynamic trait (interface-based) object directly referencing another dynamic trait (interface-based) object. The main difference between C++ O-O and Rust trait-oriented programming is C++ inheritance of implementation vs. Rust inheritance of interfaces. You can also downcast in Rust, if you try hard enough.

https://gist.github.com/rust-play/fbe471b12a0fabe4ab7b653835...


You can do references in Rust. But you can only have one mutable reference to an object at once unless you work around this with locks (Mutex, RefCell, etc). So for anything other than trivial child object references, you can quickly paint yourself into a corner of compilation errors by using webs of references. And this seems to be the basis of a lot of people's frustrations with Rust.

It's true that you also can't do inheritance. But I've found this tends to be less of a problem as most OOP languages encourage composition over inheritance anyway, and composition works the same in Rust as in other languages.


I definitely see your point about the interconnection of references, mutability and lifetime in Rust, but I guess the issue of whether Rust has a problem with the classic O-O comes down to the question of what you are trying to implement. If you are implementing a graph-like data structure (for example, UI widgets in a window), you need to keep track of multiple mutable constituent structures; in that situation, Rust references indeed are problematic for the classic O-O and you need Rc<>/RefCell<> smart pointer shenanigans. If you are implementing an actor (an algorithm, a subsystem, a process, etc. - for example, a Product, a Customer and an Order), you usually need to keep track of only a handful of structures representing the input to an actor, all of them likely immutable (a mutable actor with its input kept immutable to eliminate unexpected input side-effects); in that situation, Rust has no problem with the classic O-O, since abstractions can be expressed through references to trait objects.


The nice thing about doing it this way is that it makes it much easier to serialize and/or save to a database.


The major gist with Rust is that you only build `structs` with the basic data, and very important, stick things together with `traits` and/or `enums`.

So, for my eCommerce app I have something like

  struct Line {
    order_id: usize,
    product_name: String
  }

  
  struct Order {
    order_id: usize,
    customer: Customer,
    lines: Vec<Lines>
  }
And this means instead of pointers, using ids (alike RDBMS) is much nicer overall (work wonders that the data is already like that in RDBMS!).

You can avoid ids and just embed the data too.

Following the idea of think like in databases, when you need to find fast bring a HashMap/BtreeMap:

  struct Line {
    order_id: usize,
    product_name: String
  }

  
  struct Order {
    order_id: usize,
    customer: Customer,
    lines: BtreeMap<usize, Lines> <-- BTree nice for ordered data and range queries!
  }


This sounds to me like the journey into functional programming. I've experienced the same thing myself.


I've encountered this pretty-much every time that I've learned a new language or framework. I start out trying to use it the way I've used things before, and then I slowly learn how the people who wrote it expect me to work, and then things get a lot easier.


That too and I don't think I'm even done with that, myself :)

What I meant is the step away from the notion that things are objects, stuff gets done with methods, methods are available via mixins/ inheritance/ magic, etc.

I've seen the notion of using values and pure functions as data oriented programming recently but never understood what that moniker adds.


I think that going with pure functions/functional programming simplifies things, because it makes it much easier to test that things work as you'd expect when you don't have side-effects/state to think about.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: