Rust Any part 3: we have upcasts
174 points
2 days ago
| 7 comments
| lucumr.pocoo.org
| HN
kibwen
2 days ago
[-]
> Even though DebugAny inherits from Any

I'm going to push back against this terminology for the sake of people who don't know Rust and are coming from traditional OO languages.

Rust traits don't "inherit" from other traits, they "require" other traits.

So if I have two traits, where one requires the other:

    trait Foo {}

    trait Bar: Foo {}
That doesn't add Foo's methods to Bar's list of methods, like you might expect from the term "inheritance".

Instead, it just means that it's not possible to implement Bar for a type unless Foo is also separately implemented for that type.

Despite this it's still accurate to say that this enables a supertype/subtype relationship in certain contexts, but be careful because Rust isn't a traditionally OO language, so that may not always be the most useful framing.

reply
SkiFire13
1 day ago
[-]
> That doesn't add Foo's methods to Bar's list of methods, like you might expect from the term "inheritance".

This is not completly correct. It's true you won't e.g. be able to implement `Foo` and specify `Bar`'s method in it, however it does mean that e.g. `dyn Foo` will support `Bar`'s methods.

> Despite this it's still accurate to say that this enables a supertype/subtype relationship in certain contexts

It does enable something that looks like subtyping because you're allowed to use something that implements `Foo` when something that implements `Bar` is expected. However this is not called subtyping in Rust terms; subtyping in Rust exists but is unrelated to traits (it's only affected by lifetimes)

reply
layer8
2 days ago
[-]
It’s not clear from that description how it differs from interface inheritance in OOP.

In an interface/class hierarchy A < B < C, C can be an abstract class that only implements B’s methods and not A’s.

reply
kibwen
2 days ago
[-]
In Rust, an implementation of a trait never implements methods that weren't defined on that trait explicitly. In your example, an implementation of C inherits from B and A, but only implements B's methods. In Rust, an implementation of C cannot itself implement B's or A's methods; it is merely allowed to assume that they have been implemented elsewhere. This also means that subtraits cannot override the behavior of methods defined on supertraits.
reply
layer8
2 days ago
[-]
Okay, but doesn't Bar nevertheless inherit Foo's methods in the sense of interface inheritance? Or does code that gets passed a Bar not get access to Foo's methods?
reply
the_mitsuhiko
2 days ago
[-]
> does code that gets passed a Bar not get access to Foo's methods

Prior to the current version of Rust it was impossible to access methods on `Foo` when a `&dyn Bar` was passed. With the current beta version you can upcast.

reply
ogoffart
2 days ago
[-]
> Prior to the current version of Rust it was impossible to access methods on `Foo` when a `&dyn Bar` was passed.

This has been working since Rust 1.0, I think:

    trait Foo {
        fn foo(&self);
    }
    trait Bar : Foo {}

    impl Foo for () {
        fn foo(&self) { println!("Hello") }
    }
    impl Bar for () {}

    fn xxx(x: &dyn Bar) {
       x.foo();
    }

    pub fn main() {
        xxx(&());
    }
> With the current beta version you can upcast

Right. Now you can convert from `&dyn Bar` to `&dyn Foo` which wasn't possible before.

reply
the_mitsuhiko
2 days ago
[-]
Inherent method implementations were unavailable. (Eg: impl dyn Foo) and then call this via Bar.
reply
zamalek
1 day ago
[-]
It might be better to think of it this way: types in Rust do not implement traits, traits are implemented for types. It might seem subtle, but its not: a trait can be implemented for a type without that type's knowledge (obviously taking care for the orphan rule). Traits are also implemented via pattern matching (Into being implemented for all Froms is the most trivial example). Go's interfaces come closer to Rust traits than those from OOP languages.

I've experienced a lot of fear in other people, especially when interviewing, about Rust not being OOP. Very smart people seem to be coming up with carve-outs to keep that religion alive. Try letting go of OOP for a month or two, and you'll find that you're better off letting OOP be someone else's problem.

reply
layer8
1 day ago
[-]
I think we have different notions of OOP. OOP without implementation inheritance and with extension interfaces/methods is still very much OOP to me.
reply
Starlevel004
1 day ago
[-]
> It’s not clear from that description how it differs from interface inheritance in OOP.

Remember, when people talk about "OOP" what they're actually talking about is Java EE 6.

reply
the_mitsuhiko
2 days ago
[-]
The terminology that Rust uses is that "Foo" is a supertrait of "Bar". I understand that the docs so not call it inheritance, but from my experience at least people call this "trait inheritance" quite commonly.
reply
d4mi3n
2 days ago
[-]
What's the practical distinction here? I agree with you that rust isn't OOP, but for the sake of communication and understanding--what's the practical difference between requiring a trait and inheriting from it?
reply
Rusky
2 days ago
[-]
It means you can't just write `impl Bar for MyType` and get Foo pulled in automatically. You have to write both `impl`s yourself.

The inheritance-like syntax is shorthand for `trait Bar where Self: Foo`, and bounds like this can show up in lots of other places, where they follow the same rules: `MyType` has to impl all of them, and the traits and bounds can have fairly arbitrary (or at least un-tree-like) relationships.

The upcasting thing is a rare exception to the idea that this `Self: Foo` bound is just like any other bound.

reply
PeterWhittaker
2 days ago
[-]
If Bar inherited from Foo, Bar would have Foo's methods. So if you implemented Bar for Gum, Gum would get Bar and Foo.

But Bar requiring Foo means that if you want to use Gum in a place that expects Bar, Gum must have both Bar's methods and Foo's methods.

In some cases, you might be able to derive some of those.

reply
pjmlp
1 day ago
[-]
Rust isn't classical Java OOP, that many keep conflating with OOP, there are many ways of OOP type systems in computer science.

I have easily translanted the raytracing in one weekend from its OOP design in C++, into a similar OOP design in Rust.

reply
pwdisswordfishz
1 day ago
[-]
For one, each trait has a separate namespace for its items, so in particular Foo::quux and Bar::quux are distinct even when one trait "inherits" from the other.
reply
MuffinFlavored
1 day ago
[-]
> people who don't know Rust and are coming from traditional OO languages.

Should those people be learning Any in the first place initially as an introduction to the language? How similar to `unsafe` is `Any` in terms of "try to avoid it"?

reply
kibwen
1 day ago
[-]
If you're new to Rust, you have effectively no reason to take time out to learn about the std::any::Any type. I wouldn't say it's something to "avoid"; this is unlike unsafe, where I would advise a new programmer to actively avoid it (unless they have a C background, maybe). It's just something you don't really need, and dyn traits in Rust are still relatively verbose and restrictive enough that it naturally steers you toward more idiomatic, non-dynamic code (which isn't to say that dyn traits are useless, they have their place, but it's a niche IMO).
reply
mmastrac
2 days ago
[-]
The important change here appears to be that the internal representation of a vtable is now guaranteed to have its supertraits laid out in some predictable prefix form to its own methods.

EDIT: or if this is not possible, a pointer to the appropriate vtable is included. I assume this must be for diamond trait inheritance.

reply
kibwen
2 days ago
[-]
> the internal representation of a vtable is now guaranteed to have its supertraits laid out in some predictable prefix form to its own methods.

Importantly, note that the specifics of the vtable layout are not guaranteed, only the general property that the layout must be amenable to supporting this.

reply
the_mitsuhiko
2 days ago
[-]
And for extra context the RFC lays out the current design and future options: https://github.com/rust-lang/rfcs/blob/master/text/3324-dyn-...
reply
nialv7
1 day ago
[-]
IIRC vtable happens to already be laid out the required way (barring some corner cases maybe, correct me if I am wrong), this RFC just made that official.

And for diamond patterns vtable entries are duplicated.

reply
trashface
2 days ago
[-]
This example shows how it works for one trait, Debug, but what if you have a type that (might) implement multiple traits A, B, and/or C? It isn't clear to me if that is possible, unless the type implements all of those traits. What I'd like to do is have some base trait object and query it to see if it supports other interfaces, but also not have to have stub "I don't actually implement this" trait impls for the unsupported ones. A bit like how I might use dynamic_cast in c++.

(I believe I understand that in rust this has not historically been possible because rust doesn't have inheritance, so, there can be only one vtable for a type, rather than an chain like you might have in c++.)

reply
SkiFire13
1 day ago
[-]
> This example shows how it works for one trait, Debug, but what if you have a type that (might) implement multiple traits A, B, and/or C? It isn't clear to me if that is possible, unless the type implements all of those traits.

Yeah, you can do the same trick if you care about types that implement all of A, B and C.

> What I'd like to do is have some base trait object and query it to see if it supports other interfaces, but also not have to have stub "I don't actually implement this" trait impls for the unsupported ones.

Currently this is not possible, though it might be possible in the future once Rust gets specialization. With that you would basically be able to write the "I don't actually implement this" stub automatically. The catch however would be that this can only work for `'static` types, since such specialization for non-`'static` types is unsound.

> I believe I understand that in rust this has not historically been possible because rust doesn't have inheritance, so, there can be only one vtable for a type, rather than an chain like you might have in c++.

The issue is kinda the opposite. In Rust types can implement any number of traits (even infinite!), so they would require an potentially an infinite amount of vtables if implemented like in C++, which is just not possible. So instead this is splitted from types and moved to trait objects, which allow to carry a vtable for one specific trait.

reply
vlovich123
2 days ago
[-]
I think you’ve just confused how traits are used. They’re more like Java interfaces and there’s no inheritance - if you have trait A: B it means whatever type implements a also separately and explicitly has to implement B. Multiple traits would work similarly - either the downcast would work if the type implements a trait or you’d get back a None when you try to downcast.
reply
mmastrac
2 days ago
[-]
You can do something akin to QueryInterface from COM, though it's a bit verbose.

https://play.rust-lang.org/?version=beta&mode=debug&edition=... (ninja edited a few times with some improvements)

With a bit of API massaging, this could be improved quite a bit in terms of ergonomics. The challenge is that traits often require concrete structs if you want them to participate in dynamic typing.

Basically you can now make something like `Arc<dyn QueryInterface>` work, where multiple traits are available through a `query_interface`:

    fn main() {
        let x = Arc::new(X);
        let y = Arc::new(Y);
        let any = [x as Arc<dyn QueryInterface>, y as _];
        for any in any {
            if let Some(a) = any.query_interface::<dyn A>() {
                a.a();
            }
            if let Some(b) = any.query_interface::<dyn B>() {
                b.b();
            }
        }
    }
reply
dathinab
2 days ago
[-]
the article isn't very good for anyone not already familiar with the problem

> What I'd like to do is have some base trait object and query it to see if it supports other interfaces

> rust doesn't have inheritance

rust traits and lifetimes have inheritance, kinda, (through rust types do not)

E.g. `trait A: Debug + Any` means anything implementing A also implements Debug and Any. This is a form of interference, but different to C++ inheritance.

This is why we speaking about upcasts when casting `&dyn A as &dyn Debug` and downcast when trying to turn a `&dyn A` into a specific type.

But because this inheritance only is about traits and the only runtime type identifying information rust has is the `Any::type_id()` the only dynamic cast related querying you can do is downcasting. (Upcasting is static and possible due to changes to the vtable which allow you to turn a `&dyn A` vtable into a `dyn Any`/`dyn Debug` vtable (in the example below).

The part of bout downcast form the article you can ignore, it's some nice convenient thing implicitly enabled by the upcasting feature due to how `downcast_ref` works.

This https://play.rust-lang.org/?version=beta&mode=debug&edition=... might help.

So I think what you want might not be supported. It also is very hard to make it work in rust as you would conceptually need a metadata table for each type with pointer to a `dyn` vtable for each object safe trait the type implements and then link it in every vtable. The issue is concrete rust traits can be unbound e.g. a type `C` might implement `T<bool>` and `T<T<bool>>` and `T<T<T<bool>>>` up to infinity :=) So as long as you don't have very clever pruning optimizations that isn't very practical and in general adds a hidden runtime cost in ways rust doesn't like. Like e.g. if we look at `dyn T` it e.g. also only contains runtime type information for up casting if `T: SomeTrait` and downcasting if `T: Any` and the reason up casting took like 5+ years is because there are many subtle overhead trait offs between various vtable representations for `C: A+B` which might also affect future features etc.

reply
fcantournet
2 days ago
[-]
Any are you Debug ? Are you DebugAny ?
reply
867-5309
2 days ago
[-]
hit_by() ? struck_by() ?
reply
Ygg2
2 days ago
[-]
a_smooth_partitional()!
reply
aerzen
2 days ago
[-]
I like rust because Any is inconvenient. It pushes you to implement things with static typing.
reply
striking
2 days ago
[-]
You may want to review the article linked from within the story. The original motivation here is not just to defeat the typechecker.

It begins:

> Let's say you want to have a wrapper around a boxed any that you can debug.

reply
the__alchemist
2 days ago
[-]
Rust continues to blow my mind. It's been my most-used language for ~5 years, and I have no idea what this article is describing!
reply
jtrueb
2 days ago
[-]
Getting closer and closer to OOP
reply
GolDDranks
2 days ago
[-]
Just having features associated with OOP isn't a bad thing. Object upcasting has its uses.

It's some of the other stuff that gets OOP its bad rap.

Garbage collection, common in many OOP languages, enables having arbitrary object graphs. The programmer doesn't need to think hard about who has a reference to who, and how long these references live. This leads to performance-defeating pointer chasing, and fuzzy, not-well-thought-of object lifetimes, which in turn lead to accidental memory leaks.

Additionally, references in major languages are unconditionally mutable. Having long-lived mutable references from arbitrary places makes it hard to track object states. It makes easier to end up in unexpected states, and accidentally violate invariants from subroutine code, which means bugs.

Also, class-like functionality conflates data-layout, privacy/encapsulation, API, namespacing and method dispatch into a single thing. Untangling that knot and having control over these features separately might make a better design.

reply
jtrueb
2 days ago
[-]
I agree this is useful. I also think it isn’t the end of the world to support some semblance of OOP in Rust.

If it helps you ship the business logic, sometimes it’s okay to concede some performance or other cost.

reply
BrainInAJar
2 days ago
[-]
"sometimes" is doing a lot of work there.

Sometimes it is, but also sometimes it isn't, and Rust at least gives you a choice (you can use Arc all over the place, or if performance is critical you can be more careful about specifying lifetimes)

reply
jtrueb
1 day ago
[-]
Arc doesn’t give you the choice until this upcasting feature lands in stable.
reply
mightyham
2 days ago
[-]
You make some really good criticisms of OOP language design. I take issue with the part about garbage collecting, as I don't think your points apply well to tracing garbage collectors. In practice, the only way to have "memory leaks" is to keep strong references to objects that aren't being used, which is a form of logic bug that can happen in just about any language. Also good API design can largely alleviate handling of external resources with clear lifetimes (files, db connections, etc), and almost any decent OOP languages will have a concept of finalizers to ensure that the resources aren't leaked.
reply
tialaramex
2 days ago
[-]
Finalizers are crap. Having some sort of do-X-with-resource is much better but now you're back to caring about resource ownership and so it's reasonable to ask what it was that garbage collection bought you.

I agree with your parent that these sort of accidental leaks are more likely though of course not uniquely associated with, having a GC so that you can forget who owns objects.

Suppose we're developing a "store" web site with an OO language - every Shopper has a reference to Previous Purchases which helps you guide them towards products that complement things they bought before or are logical replacements if they're consumables. Now of course those Previous Purchases should refer into a Catalog of products which were for sale at the time, you may not sell incandescent bulbs today but you did in 2003 when this shopper bought one. And of course the Catalog needs Photographs of each product as well as other details.

So now - without ever explicitly meaning this to happen - when a customer named Sarah just logged in, that brought 18GB of JPEGs into memory because Sarah bought a USB mouse from the store in spring 2008, and at the time your catalog included 18GB of photographs. No code displays these long forgotten photographs, so they won't actually be kept in cache or anything, but in practical terms you've got a huge leak.

I claim it is easier to make this mistake in a GC language because it's not "in your face" that you're carrying around all these object relationships that you didn't need. In a toy system (e.g. your unit tests) it will work as expected.

reply
mightyham
1 day ago
[-]
> I claim it is easier to make this mistake in a GC language because it's not "in your face" that you're carrying around all these object relationships that you didn't need. In a toy system (e.g. your unit tests) it will work as expected.

I simply don't see how these mistakes would be any less obvious than in an equally complex C codebase, for instance.

reply
kortilla
2 days ago
[-]
Using a website is a strange example because that’s not at all how web services are architected. The only pieces that would have images in memory is a file serving layer on the server side and the user's browser.
reply
im3w1l
2 days ago
[-]
Agree on Finalizers being crap. Do x with resources is shit too though because I don't want indentation hell. Well I guess you could solve it by introducing a do-x-with variant that doesn't open a new block but rather attaches to the surrounding one.
reply
sham1
2 days ago
[-]
> Finalizers are crap. Having some sort of do-X-with-resource is much better but now you're back to caring about resource ownership and so it's reasonable to ask what it was that garbage collection bought you.

What garbage collection here brings you, and what it has always brought you here, is to free you from having to think about objects' memory lifetimes. Which are different from other possible resource usages, like if a mutex is locked or whatnot.

In fact, I'd claim that conflating the two as is done for example with C++'s RAII or Rust Drop-trait is extremely crap, since now memory allocations and resource acquisition are explicitly linked, even though they don't need to be. This also explains why, as you say, finalizers are crap.

Things like Python's context managers (and with-blocks), C#'s IDisposable and using-blocks, and Java's AutoCloseables with try-with-resources handle this in a more principled manner.

> I agree with your parent that these sort of accidental leaks are more likely though of course not uniquely associated with, having a GC so that you can forget who owns objects. > > Suppose we're developing a "store" web site with an OO language - every Shopper has a reference to Previous Purchases which helps you guide them towards products that complement things they bought before or are logical replacements if they're consumables. Now of course those Previous Purchases should refer into a Catalog of products which were for sale at the time, you may not sell incandescent bulbs today but you did in 2003 when this shopper bought one. And of course the Catalog needs Photographs of each product as well as other details.

Why are things like the catalogues of previous purchases necessary to keep around? And why load them up-front instead of loading them lazily if you actually do need a catalogue of incandescent light bulbs from 2003 for whatever reason?

> So now - without ever explicitly meaning this to happen - when a customer named Sarah just logged in, that brought 18GB of JPEGs into memory because Sarah bought a USB mouse from the store in spring 2008, and at the time your catalog included 18GB of photographs. No code displays these long forgotten photographs, so they won't actually be kept in cache or anything, but in practical terms you've got a huge leak.

I'm sorry, what‽ Who is this incompetent engineer you're talking about? First of all, why are we loading the product history of Sarah when we're just showing whatever landing page we show whenever a customer logs in? Why are we loading the whole thing, instead of say, the last month's purchases? Oh, and the elephant in the room:

WHY THE HELL ARE WE LOADING 18 GB OF JPEGS INTO OUR OBJECT GRAPH WHEN SHOWING A GOD-DAMN LOGIN LANDING PAGE‽ Instead of, you know, AT MOST JUST LOADING THE LINKS TO THE JPEGS INSTEAD OF THE ACTUAL IMAGE CONTENTS!

Nothing about this has anything to do with whether a language implementation has GC or not, but whether the hypothetical engineer in question, that wrote this damn thing, knows how to do their job correctly or not.

> I claim it is easier to make this mistake in a GC language because it's not "in your face" that you're carrying around all these object relationships that you didn't need. In a toy system (e.g. your unit tests) it will work as expected.

I don't know, if the production memory profiler started saying that there's occasionally a spike of 18 GB taken up by a bunch of Photograph-objects, that would certainly raise some eyebrows. And especially since in this case they were made by an insane person, that thought that storing the JPEG data in the objects themselves is a sane design.

---

As you said above, this is very much not unique to language implementations with GC. Similar mistakes can be made for example in Rust. And you may say that no competent Rust developer would do this kind of a mistake. And that's almost certainly true, since the scenario is insane. But looking at the scenario, the person making the shopping site was clearly not competent, because they did, well, that.

reply
vlovich123
2 days ago
[-]
Why do you treat memory allocations as a special resource that should have different reasoning about lifetime than something like a file resource, a db handle, etc etc? Sure if you don’t care about how much memory you’re using it solves a small niche of a problem for a lot of overhead (both CPU and memory footprint) but Rust does a really good job of making it easy (and you rarely / never really have to implement Drop).

The context managers and stuff are a crutch and admittance that the tracing GC model is flawed for non memory use cases.

reply
ablob
2 days ago
[-]
Memory is freed as soon as the program closes by the operating system. If you have an open connection to a database they might expect you to talk to them before you close the handle (for example, to differentiate between crash and normal shutdown). Any resource shared between programs might have a protocol that needs to be followed which the operating system might not do for you.

The GC model only cares about memory, so I don't really understand what you mean by "flawed for non memory use cases". It was never designed for anything other than managing memory for you. You wouldn't expect a car to be able to fly, would you?

I personally like the distinction between memory and other resources. If you look hard enough I'm sure you'll find ways to break a model that conflates the two. Similar to this, the "everything is a file" model breaks down for some applications. Sure, a program designed to only work with files might be able to read from a socket/stream, but the behavior and assumptions you can make vary. For example, it is reasonable to assume that a file has a limited size. Imagine you're doing a "read_all" on some video-feed because /dev/camera was the handle you've been given. I'm sure that would blow up the program.

In short, until there is a model that can reasonably explain why memory and other resources can be homogenized into one model without issue, I believe it's best to accept the special treatment.

reply
vlovich123
1 day ago
[-]
> to differentiate between crash and normal shutdown

> Any resource shared between programs might have a protocol that needs to be followed which the operating system might not do for you.

Run far and quickly from any software that attempts to do this. It’s a sure fire indication of fragile code. At best such things should be restricted to optimizing performance maybe if it doesn’t risk correctness. But it’s better to just not make any assumptions about reliable delivery indicating graceful shutdown or lack of signal indicating crash.

> In short, until there is a model that can reasonably explain why memory and other resources can be homogenized into one model without issue, I believe it's best to accept the special treatment.

C++ and Rust do provide compelling reasons why memory is no different from other resources. I think the counter is the better position - you have to prove that a memory resource is really different for the purposes of resource management than anything else. You can easily demonstrate why network and files probably should have different APIs (drastically different latencies, different properties to configure etc). That’s why network file systems generally have such poor performance - the as-if pretension sacrifices a lot of performance.

The main argument tracing GC basically makes is that it’s ok to be geeedy and wasteful with RAM because there’s so much of it that retaining it extra is a better tradeoff. Similarly it argues that the cycles taken by GC and random variable length pauses it often generates don’t matter most of the time. The counter is that while it probably doesn’t matter in the P90 case, it does matter when everyone takes this attitude and P95+ latencies (depending on how many services are between you and the user you’d be surprised how many 9s of good latency your services have to achieve for the eyeball user to observe an overall good P90 score).

> For example, it is reasonable to assume that a file has a limited size. Imagine you're doing a "read_all" on some video-feed because /dev/camera was the handle you've been given. I'm sure that would blow up the program.

Right, which is one of many reasons why you shouldn’t ever assume files you’re given are finite length. You could be passed stdin too. Of course you can make simplifications in cases, but that requires domain-specific knowledge, something tracing GCs do not have because they’re agnostic to use case.

reply
Mawr
2 days ago
[-]
Because 99.9% of the time the memory allocation I'm doing is unimportant. I just want some memory to somehow be allocated and cleaned up at some later time and it just does not matter how any of that happens.

Reasoning about lifetimes and such would therefore be severe overkill and would occupy my mental faculties for next to zero benefit.

Cleaning up other resources, like file handles, tends to be more important.

reply
vlovich123
1 day ago
[-]
Then stick it in a Box, RC or Arc and forget about it. You only need lifetimes for cases where you want to avoid heap allocations.
reply
int_19h
1 day ago
[-]
Simple reference counting fails as soon as you have a graph that might have loops in it.
reply
vlovich123
1 day ago
[-]
Then use “tracing GC as a library” like [1] or [2]. I’m not saying there’s no use for tracing GC ever. I’m saying it shouldn’t be a language level feature and it’s perfectly fine as an opt-in library.

[1] https://github.com/Manishearth/rust-gc

[2] https://github.com/oilpan-gc/cppgc

reply
int_19h
18 hours ago
[-]
Bolt-on GCs necessarily have to be conservative about their assumptions, which significantly hinders performance. And if language semantics doesn't account for it, the GC can't properly do things like compacting (or if they can, it requires a lot of manual scaffolding from the user).

It should be a language level feature in a high-level language for the simple reason that in vast majority of high-level code, heap allocations are very common, yet most of them are not tied to any resource that requires manual lifetime management. A good example of that is strings - if you have a tracing GC, the simplest way to handle strings is as a heap-allocated immutable array of bytes, and there's no reason for any code working with strings to ever be concerned about manual memory management. Yet strings are a fairly basic data type in most languages.

reply
vlovich123
1 hour ago
[-]
That's my point though. You either have graph structures with loops where the "performance" of the tracing GC is probably irrelevant to the work you're doing OR you have graph ownership without loops. The "without loops" is actually significantly more common and the "loops" case actually has solutions even without going all the way to tracing GC. Also, "performance" has many nuances here. When you say "bolt on GC significantly hinders performance", are you talking about how precisely they can reclaim memory / how quickly after being freed? Or are you talking about the pauses the collector has to make or the atomics it injects throughout to do so in a thread-safe manner?

I suspect the benefits of compaction are wildly overstated because AFAIK compaction isn't cache aware and thus the CPU cache thrashes. By comparison, a language like Rust lets you naturally lay things out in a way that the CPU likes.

> if you have a tracing GC, the simplest way to handle strings is as a heap-allocated immutable array of bytes

But what if I want to mutate the string? Now I have to do a heap allocation and can't do things in-place. Memory can be cheap to move but it can also add up substantially vs an in-place solution.

reply
jghn
2 days ago
[-]
> Garbage collection, common in many OOP languages

I would argue this is correlation, not causation. And of the many flaws one can raise with OOP, that GC is pretty low on that list.

reply
Xeoncross
2 days ago
[-]
Are we talking about functional OOP or class-less OOP or JavaScript's prototypal version of inheritance in OOP or Javas functions-are-evil OOP or some other version of OOP?

Object-Oriented Programming is a spectrum with varying degrees of abuse.

reply
bigstrat2003
2 days ago
[-]
OOP is a good thing, not a bad thing. It enables you to use objects to represent relationships easily in a way that other paradigms don't. I agree that the trendiness of OOP was stupid (it isn't the right approach to every situation and it was never going to solve all our problems), but the way it's trendy to hate on OOP now is equally stupid. It's a good tool that sometimes makes sense and sometimes doesn't, not a savior or a devil.
reply
vlovich123
2 days ago
[-]
“Inheritance” of interfaces good *. Inheritance of stateful objects bad - composition is much better. The OOP model that Rust supports only supports the good kind of OOP and doesn’t support the bad kind.

* technically rust doesn’t have interface inheritance but you can treat it that way and it’ll mostly look like that in an abstract sense.

reply
jandrewrogers
2 days ago
[-]
I agree that composition is much better than inheritance for most typical code cases. However, there are cases where inheritance is absolutely the correct model and not having inheritance makes for worse code. I may not use inheritance very often (it rarely makes sense in a systems context) but it is nice to have it when it is unambiguously the right tool for the job.

Most code paradigms exist because there is some code context where they are nearly ideal. Someone invented them to handle that case efficiently and elegantly. If you write diverse software long enough you will come across all of those contexts in real code.

I’m the opposite of a model purist. I have a strong preference for a languages that let you efficiently switch models on a very granular basis as may be useful in the moment.

reply
zozbot234
2 days ago
[-]
> The OOP model that Rust supports only supports the good kind of OOP and doesn’t support the bad kind.

Well, almost. You can actually express "the bad kind of OOP" (i.e. implementation inheritance) in Rust quite elegantly via the generic typestate pattern! But that's still good, because it reveals the inherent complexity of your 'inheritance hierarchy' very clearly by modeling every part of it as a separate, possibly generic type. Hence why it's very rarely used in Rust, with interface inheritance being preferred instead. It mostly gets used in cases where the improved static checking made possible by the "typestate" pattern can be helpful, which has little to do with "OOP" design as generally understood.

reply
dullcrisp
2 days ago
[-]
I love how OOP is considered a slur on here. It’s no wonder Alan Kay doesn’t come back.
reply
Rexxar
2 days ago
[-]
Why do you interpret this comment as anti-OOP ?

I see it has criticising rust choice to not do OOP at the beginning to finally do it piece by price and that it would have probably be better for the language to embrace it from start.

reply
dullcrisp
2 days ago
[-]
I don’t know how to interpret the comment, but that seems to be how the other responses interpret it.
reply
bfrog
2 days ago
[-]
Inherit is the first step to tell.
reply
dullcrisp
1 day ago
[-]
I don’t disagree about inheritance.
reply
quotemstr
2 days ago
[-]
There's this dynamic in the industry in which a brash young project comes out swinging against some established technique and commits early to its opposite. Then, as the project matures, its authors begin to understand why the old ways were the way they were and slowly walk back their early stridency --- usually without admitting they're doing it.

Consider Linux. Time was, metaprogramming was BAD, C was all you needed, we didn't need dynamic CPU schedulers, we didn't need multiple LSMs, and we sure as hell didn't need ABI stability. Now we have forms of all of these things (for the last, see CO-RE), because as it turns out, they're actually good.

In Python, well, turns out multiprocessing isn't all you need, and a free-threaded Python has transitioned from impossible and unwanted to exciting and imminent.

In transfer encodings, "front end" people thought that JSON was all you needed. Schemas? Binary encodings? Versioning? References? All for those loser XML weenies. Well, who's the weenie now?

And in Rust? Well, let's see. Turns out monomorphization isn't all you need. Turns out that it is, in fact, occasionally useful to unify an object and its behavior in a runtime-polymorphic way. I expect yeet_expr to go through eventually too. Others are trying to stabilize (i.e. ossify) the notionally unstable ABI, just like they did to poor C++, which is now stuck with runtime pessimization because somebody is too lazy to recompile a binary from 2010.

As surely as water will wet and fire will burn, the gods of Java with stupidity and complexity return.

reply
Rusky
1 day ago
[-]
> And in Rust? Well, let's see. Turns out monomorphization isn't all you need. Turns out that it is, in fact, occasionally useful to unify an object and its behavior in a runtime-polymorphic way. I expect yeet_expr to go through eventually too. Others are trying to stabilize (i.e. ossify) the notionally unstable ABI, just like they did to poor C++, which is now stuck with runtime pessimization because somebody is too lazy to recompile a binary from 2010.

Not to make an argument either way on your general point, but these are really bad examples for Rust if you look at the specifics:

Monomorphization was never the only option. Trait objects and vtables predate the RFC process itself. Early Rust wanted more dynamic dispatch than it eventually wound up with.

The idea of a "throw"-like operator was introduced at the same time as the `?` operator and `try` blocks: https://rust-lang.github.io/rfcs/0243-trait-based-exception-... (Okay, technically `?` was proposed one month previously.)

All the various initiatives related to stable ABI are focused on opt-in mechanisms that work like `#[repr(C)]` and `extern "C"`.

The only way to interpret these as examples of "brash young project walks back its early stridency as it ages" is if you ignore the project's actual reasoning and design choices in favor of the popular lowest-common-denominator Reddit-comment-level understanding of those choices.

reply
pcwalton
1 day ago
[-]
> Turns out monomorphization isn't all you need. Turns out that it is, in fact, occasionally useful to unify an object and its behavior in a runtime-polymorphic way.

This actually gets the history backwards. Ancient Rust tried to do generics in a fully polymorphic way using intensional type analysis, like Swift does. We switched to monomorphization reluctantly because of the code bloat, complexity of implementation, and performance problems with intensional type analysis. "dyn Trait" was always intended to be an alternative that code could opt into for runtime polymorphism.

reply
diggan
2 days ago
[-]
At least they're not expanding the standard library for reading emails, yet...
reply
the_mitsuhiko
2 days ago
[-]
I'm a bit confused by both this comment and the previous one. Fundamentally nothing new is unlocked by this, that wasn't already possible for many years. It's just the ergonomics that got much better through this change.
reply
BerislavLopac
2 days ago
[-]
The second comment refers to Zawinski's Law [0].

[0] https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law

reply
tialaramex
2 days ago
[-]
I can't speak for your parent but I'm aware of Zawinski's Law and I could see that's what the comment was about, but like your parent it's not at all clear to me why, it's a non-sequitur - this is giving Rust a convenient safe way to up cast, that's not anywhere in the ballpark of the sort of "expand to do everything" that Jamie was describing as inevitable.

If you say "Ooh, the new kitten is learning how the cat flap works" and I respond "Those who cannot remember the past are condemned to repeat it" and then you respond with confusion that's because my quoting of Santayana is a non-sequitur. I might even agree with his sentiment about the importance of history, but the kitten learning how to use a cat flap isn't anywhere close to what Santayana was talking about so...

reply
diggan
2 days ago
[-]
My jest wasn't meant to say that Rust is expanding to do everything, but rather the opposite. The comment I replied to somehow seems to believe Rust is becoming more "OOP", so I took it a step further and also referenced a fairly known pitfall for platforms (so doesn't even apply to programming languages, in my mind).

In the end, it's a joke with no even a pinch of truth, which seems to have landed flat, that's on me I suppose.

reply
capitol_
2 days ago
[-]
Since rust is popular it naturally attracts a counter movement, so some people will grasp at straws in order to say something negative.
reply
echelon
2 days ago
[-]
Rust has had significant OOP features since the beginning. Trait methods, dynamic dispatch, inheritance, bounds, etc.
reply
__s
2 days ago
[-]
Do you consider Go OOP?
reply