in Rust, methods should be object safe
nora.codes
nora.codes
> I think we should use “method”, in Rust
Just don't. Object-oriented programming is a 1980s buzzword. Let's help industry catch up.
I don't care if people call them functions or methods in daily speech. I even do that sometimes, depending on whom I'm talking to.
But let's not make up a highly technical definition of what a Rust method is.
Methods are loosely the same as functions that take a '&self', and only because your brain had that word for it, not because it's a good word.
Isn't this just true of any word with increased specificity? Why call it a square when we already have rectangle?
I'm not really seeing how understanding method vs. function is "highly technical". At least, not more technical than, I don't know, writing Rust code in the first place?
In academic circles, there’s some meaningful distinctions to be made. In software engineering I’ve yet to find a place where having a very specific name mattered or reduced communication burden.
Though I do tend to work with teams where the average age is higher than the industry average, that might be warping my anecdotal experiences
I have had situations where the distinction matters, and they aren't academic either, but in API design, especially for libraries
I am aware of the distinction and I (probably?) instinctively use the two terms “correctly” the majority of the time. But if I just picked one or the other and used that word 100% of the time I would wager there would be zero confusion, except amongst pedants.
For what it’s worth, I say this as an avowed pedant.
Because _you_ haven’t means the entire industry is wrong?
My point is only that fretting over the distinction between the words serves nearly zero utility in day to day software engineering. It’s like “less than” vs. “fewer”. Each has a correct use, but nobody is confused when you use the wrong one. Many confuse the two, to the point that it’s becoming less and less wrong to use one when the other should be used. “Method” and “function” are similar.
In twenty years, nobody is going to care that they once had a distinctive purpose because that distinction serves little utility.
Small children say things incorrectly and we still understand them. Should we all talk like that?
If a clear separation in meaning between the two words was useful, their distinct use would become more closely adhered to over time. If not, people will start to blend and confuse the two. Of those options, the latter has happened and will likely continue happening with or without your objections.
That this has happened is pretty clear evidence that the distinction hasn’t been worth maintaining.
> Small children say things incorrectly and we still understand them. Should we all talk like that?
Fully grown, articulate adults say things “incorrectly” all the time and nobody bats an eye. The sentence “Hey, where you going?” is missing an entire verb and every one of us has said and heard that sentence (or one like it) hundreds of times without anyone noticing or caring.
So, when you’re talking to your coworkers about…whatever… the context probably makes method vs function insignificant. Unless you work in language design, in which case your take is just pretty blatantly awful.
The word "method" is baked into the official Rust docs[0], and while I personally find the definition in the docs to be satisfying, I don't think the author's argument for narrowing that definition down is unreasonable or (more importantly) unworthy of discussion.
[0] https://doc.rust-lang.org/rust-by-example/fn/methods.htmlI am not super familiar with the definition of "object-safe" (I read the docs but the definition is not completely trivial for me).
Does it work to say that an "object-safe" method mutates the object? That's what happens when you take `&mut self`, right?
But if I understood this correctly, I guess it means that a function without side-effect in Java should not really count as a method? Because in Java I call it a method whether it has a side-effect or not. Which corresponds exactly to the definition of `method` in the Rust docs, I think [1]. In C++, I would think that this is the difference between a const (no side-effect) and a non-const (side-effect) method.
[1]: https://doc.rust-lang.org/reference/items/associated-items.h...
Yes. In the reference, below the definition, there is a list of examples of:
- Traits that are object-safe with object-safe methods
- Traits that are object-safe with non-dispatchable methods
- Traits that are not object-safe
https://doc.rust-lang.org/reference/items/traits.html#object...> you can make a `Vec<YourType>` as long as `YourType` does not implement any non-object-safe methods?
It is traits that are object safe, not concrete types.
You cannot make a `Vec<YourTrait>`, you have to make a `Vec<T> where T: YourTrait` or a `Vec<Box<dyn YourTrait>>` or similar.
You only need object safety if you're passing around values dynamically cast to a trait. That's not a required programming pattern.
Yep. In 3+ years of writing rust full time, I’ve still never used `dyn Trait` in any of its forms. A vec where every item is independently heap allocated? Why? And yikes! That would perform terribly.
Just because that's the example given (by someone who's trying to understand the concept, no less) doesn't mean that that's the only thing that the language feature is usable for.
Trait objects (read: runtime polymorphism) have their pros and cons just as generics (read: parametric polymorphism) have theirs. Not everything can be known at compile time, and that's perfectly okay.
It kind of makes me we wish there was a keyword that would grab all implementations of that trait being compiled and do the same thing though. At least for a statically linked codebase it could figure out all the sizes of the implementations
But it would be nice if there was some unification of the idea of an enum where every variant implements a trait and &dyn Trait. Basically, an enum made up of a set of named structs where each struct implements a specific trait could be a lovely concept.
I used to care so much about this stuff then I learned to not give a sh*t and Arc<dyn Trait> to get stuff done. If you use a service oriented architecture with interfaces to reduce coupling this is the way.
A trait is object-safe as long as no trait method returns the implementing type (like T::clone() -> T). Once it's a trait object, T is no longer known and the compiler doesn't know how much memory to allocate on the stack or heap, which makes it impossible to use. That's why trait objects have to be boxed or behind references - all pointers have a known size.
That, and much more: https://doc.rust-lang.org/reference/items/traits.html#object... .
If OOP encompasses both Smalltalk AND Rust, it's like saying your definition of X includes a neutron star and giraffe.
By OOP type theory, you mean category theory?
----
Most what people recognize as OOP today is just Class/Inheritance oriented programming, and Rust doesn't belong in that camp, trying to use structural patterns from that kind of OOP in Rust is often a recipe for disaster.
Not what most people on the street think OOP is all, with their anti-Java bias, rather the Computer Science studies of type systems and their application across all forms of OOP in all programming languages ever invented.
Rust has enough features from OOP type systems, regardless of the anti-OOP feeling trying to pretend it doesn't.
Static and dynamic polymorphism, methods, data encapsulation, type dispatch, all there.
Tell that to Google. OOP type theory returns mostly Category type theory as results.
What sources do you have on OOP type theory? Our OOP classes were heavy on practical part and less so on theory.
> Rust has enough features from OOP type systems
It's missing inheritance. Without inheritance applying OOP design patterns just fails miserably.
So on one side you have stuff like Java, C++, JS, Ruby, Python and on the other Rust.
> Static and dynamic polymorphism, methods, data encapsulation, type dispatch, all there.
By that definition, Python isn't object oriented (no encapsulation), and C is.
I think it's much more natural to reason about types on what they can do versus some nebulous definition.
I mean any language worth its salt will have encapsulation. It's how you preserve your invariants.
Method is mostly just syntax sugar.
And static/dynamic polymorphism is just a fancy way to say we have function overloading and/or templates and/or vtables (even if using pointer casts).
Class inheritance is not a requirement for OOP languages.
I listed the OOP traits supported by Rust as a language.
Naturally you have to nitpick it to try to make a point out of nothing.
All high level languages are fancy ways to write machine code.
So is the list missing items?
> Naturally you have to nitpick it to try to make a point out of nothing.
What did I nitpick? Does Python have encapsulation beyond gentlemen's agreement?
Can't C have any of the listed behaviors?
However, in case you need some help, in regards to Python
Classe inheritance, function and data members, dynamic dispatch, polymorphism, meta-classes.
And even if C doesn't offer direct language support for a OOP type system, that isn't stopping anyone, like the Linux kernel and GNOME/Gtk+.
Or publishing books on the matter,
http://www.freetechbooks.com/object-oriented-programming-wit...
No. Bold of you to assume that. I wasn't aware that one OOP isn't another OOP. I see, so OOP is a fuzzyish definition that you check more checkboxes to satisfy.
If Rust is OOP, and Python is OOP and C is OOP, then the that circles back to my initial claims. A category that is so wide it's effectively useless.
Compare this to sync and async. A precise definition, much greater informational content. You can prove something is pure or not[1]. Compare this to OOP, imprecise, less informational context, impossible to (dis)prove something is OOP or not.
https://www.researchgate.net/figure/Class-Association_fig3_2...
What Rust is not: class-based or inheritance-based (discounting default trait impls).
And this is a nice, modern form of OOP that discards orthogonal and useless design baggage.
Rust rapidly becomes painful if you try to OOP in it. You would be better off using an OOP language for OOP. It wasn't voted the most loved language because people enjoyed learning lifetimes alongside all the accidental complexity OOP creates.
Dynamic dispatch is mostly seen in two circumstances: the type can't be known at compile time, and reducing code bloat as an optimization step.
Likewise, Microsoft teams not only didn't had any major issues adding COM tooling support for Rust, it is much easier to deal with COM in Rust, than all the C++ frameworks Microsoft has created throughout the years.
Likewise, code available on the official Windows bindings for Rust.
This doesn't work for every case of dynamic dispatch (it only works for what I call closed and semi-open universes of choices [1]), but it works for many of them. And there can always be a catch-all variant that uses a trait object.
In college, I (and I'm sure many others here) learned that OOP was about "abstraction, encapsulation, inheritance, and polymorphism".
But in practice, looking at the history of programming paradigms in industry, it seems like the real success of OOP was in dragging us away from "it's fine for mutable state to be global and/or scattered willy-nilly throughout your code" and towards "mutable state should be localized, tightly scoped, and/or coupled to the code that will be mutating it". Being generous to the academic definition, we could say that the only pillar of OOP that really mattered was encapsulation.
And by that definition, since Rust abhors global mutable state (but not mutation in general, as a functional language might do), it's not unreasonable to say that Rust, even if it doesn't have "objects" as Java or Smalltalk define them, still hews to the most useful parts of OOP.
Typical enterprise OOP did the opposite of this, coupling state to unrelated code via inheritance. To quote the creator of Erlang, when you want the banana it gives you the whole forest.
Ironically, the way to do actual encapsulation / localized-mutable-state is to just use stack variables - the C way.
Fields (what OOP added) are leakier and less localized than stack variables.
The problem is that C doesn't offer any resistance to using globals rather than stack variables, which led to much of the mess that the OOP revolution of the 80s was supposed to address. For every C program that eschews globals, you have a Toyota Camry, whose control systems famously contained "10,000 global variables" https://news.ycombinator.com/item?id=9643204
Nor is there any resistance to writing 'public static' on a field anywhere in Java land. If that's a problem, you have to throw out most languages.
Who needs the private keyword when C has better encapsulation than Java?
-- foo.h
typedef struct Bleh Bleh_t;
Bleh_t *construct(int i);
int getFoo(Bleh_t *bleh);
-- main.c
#include "foo.h"
void main() {
Bleh_t *bleh = construct(3);
// int i = bleh->foo;
int i = getFoo(bleh);
}
The commented-out line above will not compile, because the internals of Bleh_t are not exposed. typedef struct Bleh Bleh_t;
size_t get_bleh_size();
void init_bleh(Bleh_t *, int);
int main(void) {
const size_t bleh_size = get_bleh_size();
Bleh_t *const bleh = alloca(bleh_size);
init_bleh(bleh, 3);
const int i = getFoo(bleh);
}
If you rewrite "init_bleh" to return the pointer it receives as the first parameter, you could hide it behind a macro: #define make_stack_bleh(i) init_bleh(alloca(get_bleh_size()), (i))
Used like this: Bleh *const bleh = make_stack_bleh(x);
One advantage of this approach is that you could change the layout of a Bleh without recompiling the other object files, as your compiled code makes no assumptions about the size or the layout of the struct (i.e. the field offsets into a struct aren't hardcoded into the machine code emitted).This is what I was referring to. A problem that follows is that many of the Go4 patterns solve problems that OOP causes in the first place (obviously many others are universal).
// counter_associated.rs
[...]
let c = Counter::increment(f);
What is `f` here?Or, write in Markdown and use tooling to perform tests the same way Rustdoc does, if you wrote what claims to be code, just scoop it up and execute it, if it doesn't compile then we find out before we publish.
I'd say 80% of the time when I write more than one or two lines I write a bug, often it's a minor typo that the compiler would catch but sometimes it's a larger mistake and means I should re-evaluate my entire premise.