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.
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.
Code here: https://github.com/prisma/prisma-engines/tree/main/libs%2Fda...
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.
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".
If it was (2), it sounds like you made it work without unsafe code? Can you share some code?
What is better about them?
Sounds kind of like Stockholm syndrome
Seriously, I sometimes wonder what is wrong with the whole field of software development.
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).
struct B {
a: A
}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.
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.
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.
Otherwise you end up with Haskell is an OOP language.
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).
Counter example: Html5 DOM.
A graph representation of nodes based on JavaScript object model?
Fine, https://rustwasm.github.io/docs/wasm-bindgen/examples/dom.ht...
It's a brittle simulation of Inheritance. To transform a saying "Just because you can write Ada in C, doesn't make C Ada".
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.
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.
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.
Go has it's strengths, but IMO fun isn't one of them. It turns into soul-suck quickly.
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.
---
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 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...
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.
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!
}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.
When I tried again recently I started with the Too Many Linked Lists, which I felt did a better job of explaining both lifetimes and how the compiler views <type>, &<type>, and &ref <type> as completely different types, and that helped a ton.
Combine that with spending more time understanding how Rust handles object composition (compared to "traditional" OOP), the massive compiler improvements W/R/T lifetime elision and auto-derefing within the past several years, and realizing just how damn helpful the compiler and documentation is compared to other languages (again, thanks to TMLL for explicitly showing this), and it's been a downright pleasure to use this time around.
I strongly suspect that it will stick this time.
It was very valuable, say, twenty years ago, when there were most programs were written or compiled using multiple closed source implementations of languages coming from competing companies. There were real economic incentives for the implementations to diverge from each other in ways that harmed the larger ecosystem. A formal spec was a forcing function to make those implementations compatible.
Today, programmers simply won't use a language whose implementation isn't open source with a very permissive license. This makes it very hard for an organization to deliberately make the implementation incompatible with others because other organizations and users are able to either avoid the incompatibility by forking the implementation if they don't like it, or making it compatible by using the implementation if they do.
I still think it's very valuable to have a committee with members of all implementations that helps drive consensus for where the language should go. But the document itself I see as secondary to that human process.
About 2 months ago I wanted to use Arrow within Elixir, which required me to start using Rust again (Elixir uses Rustler to safely convert from Rust <> Elixir without theoretically crashing the beam). I am amazed how far it's come, and I've actually been using it a lot over the last few months and haven't ran into any major issues. There is a fair amount of information out there, and the documentation as the OP pointed out is really fantastic.
The libraries are really good now, and I've had no issues using it on Linux or on my Macbook M1. Incredibly impressive speed results. SIMD is a game changer.
Major shoutouts to arrow2 and Polars, I love using it for data analysis :).