[HN Gopher] Rust Any part 3: we have upcasts
___________________________________________________________________
Rust Any part 3: we have upcasts
Author : jmillikin
Score : 154 points
Date : 2025-03-30 11:15 UTC (11 hours ago)
(HTM) web link (lucumr.pocoo.org)
(TXT) w3m dump (lucumr.pocoo.org)
| jtrueb wrote:
| Getting closer and closer to OOP
| diggan wrote:
| At least they're not expanding the standard library for reading
| emails, yet...
| the_mitsuhiko wrote:
| 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.
| BerislavLopac wrote:
| The second comment refers to Zawinski's Law [0].
|
| [0]
| https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law
| tialaramex wrote:
| 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...
| diggan wrote:
| 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.
| capitol_ wrote:
| Since rust is popular it naturally attracts a counter
| movement, so some people will grasp at straws in order to
| say something negative.
| GolDDranks wrote:
| 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.
| jtrueb wrote:
| 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.
| BrainInAJar wrote:
| "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)
| jtrueb wrote:
| Arc doesn't give you the choice until this upcasting
| feature lands in stable.
| mightyham wrote:
| 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.
| tialaramex wrote:
| 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
| _Photograph_ s 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.
| kortilla wrote:
| 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.
| sham1 wrote:
| > 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 _Photograph_ s 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.
| vlovich123 wrote:
| 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.
| ablob wrote:
| 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.
| Mawr wrote:
| 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.
| im3w1l wrote:
| 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.
| jghn wrote:
| > 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.
| __s wrote:
| Do you consider Go OOP?
| dullcrisp wrote:
| I love how OOP is considered a slur on here. It's no wonder
| Alan Kay doesn't come back.
| Rexxar wrote:
| 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.
| dullcrisp wrote:
| I don't know how to interpret the comment, but that seems
| to be how the other responses interpret it.
| bfrog wrote:
| Inherit is the first step to tell.
| Xeoncross wrote:
| 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.
| bigstrat2003 wrote:
| 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.
| vlovich123 wrote:
| "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.
| jandrewrogers wrote:
| 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.
| zozbot234 wrote:
| > 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.
| echelon wrote:
| Rust has had significant OOP features since the beginning.
| Trait methods, dynamic dispatch, inheritance, bounds, etc.
| quotemstr wrote:
| 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.
| Rusky wrote:
| > 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.
| pcwalton wrote:
| > 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.
| mmastrac wrote:
| 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.
| kibwen wrote:
| _> 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.
| the_mitsuhiko wrote:
| 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-...
| nialv7 wrote:
| 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.
| fcantournet wrote:
| Any are you Debug ? Are you DebugAny ?
| 867-5309 wrote:
| hit_by() ? struck_by() ?
| Ygg2 wrote:
| a_smooth_partitional()!
| aerzen wrote:
| I like rust _because_ Any is inconvenient. It pushes you to
| implement things with static typing.
| striking wrote:
| 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.
| trashface wrote:
| 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++.)
| vlovich123 wrote:
| 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.
| mmastrac wrote:
| 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(); }
| } }
| dathinab wrote:
| 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.
| SkiFire13 wrote:
| > 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.
| kibwen wrote:
| _> 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.
| d4mi3n wrote:
| 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?
| Rusky wrote:
| 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.
| PeterWhittaker wrote:
| 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.
| pwdisswordfishz wrote:
| 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.
| pjmlp wrote:
| 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.
| layer8 wrote:
| 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.
| kibwen wrote:
| 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.
| layer8 wrote:
| 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?
| the_mitsuhiko wrote:
| > 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.
| ogoffart wrote:
| > 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.
| the_mitsuhiko wrote:
| Inherent method implementations were unavailable. (Eg:
| impl dyn Foo) and then call this via Bar.
| zamalek wrote:
| 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.
| layer8 wrote:
| I think we have different notions of OOP. OOP without
| implementation inheritance and with extension
| interfaces/methods is still very much OOP to me.
| Starlevel004 wrote:
| > 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.
| the_mitsuhiko wrote:
| 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.
| SkiFire13 wrote:
| > 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)
| the__alchemist wrote:
| 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!
___________________________________________________________________
(page generated 2025-03-30 23:00 UTC)