Hacker News new | ask | show | jobs
by steveklabnik 2000 days ago
dyn just requires the object to be behind some kind of pointer. The vast majority of the time, that pointer is a Box or an Rc/Arc, but any form of indirection can work.
1 comments

Taking a second look at this particular example, I guess it is a bit of a "trick" because of the ahead-declaration of both possible "holder" variables on the stack. It's just... wildly unintuitive that this should be possible. Even if you showed me this code without saying whether or not it should compile, I wouldn't be sure.

Here's the train of intuition:

1) dyn requires a pointer that may be to one of multiple types of structs

2) a group of multiple types of structs has an undefined memory layout, so the value must either live on the heap or be wrapped up in an enum

That feels like an airtight understanding. But then Rust lets you do this weird juggling maneuver based on control-flow that allows you to do it on the stack.

I'm not saying Rust shouldn't let you do this, and I'm not really sure how it could be made intuitive given the "normal" case. I'm just expressing that subjectively, this feels very weird and non-obvious, and it's far from the first example like this that I've encountered. Here's another example: https://news.ycombinator.com/item?id=25595120

I think where your intuition is leading you wrong is that in #2, you are assuming ownership. That is, you're saying "I need a place to put an arbitrary thing." But you don't! A &dyn T doesn't own T, just like a &T doesn't own T. Trait objects are a (pointer to data, pointer to vtable), (may be in the reverse order we don't guarantee layout) and so that pointer can point to anywhere, heap or stack.

Interestingly enough, this was special in Rust 1.0 to Rust 1.5. In 1.5, it finally became non-special. It's interesting because I totally get what you're saying, but at the same time, this is an example of Rust being orthogonal, not special cased.

(It is really hard to address the string thing without an example, to be honest.)

I think you're right about my intuition, and that's interesting about the syntax change. It could just be that during my learning stage I learned (based on example bias maybe?) that dyn is for owned values, and not just any reference. I'm sure this was never stated explicitly, but somehow that idea got lodged in my brain

For the String thing (really, the Deref thing), further down others weigh in with a case where it doesn't work as expected, and then the workaround &*. The latter is something that feels like it should be a non-op, yet it's required in certain cases like this one to trigger something in the compiler. I'm sure there's some internal reason for this, but from the user's perspective it's, "What does dereferencing and then re-referencing this value have to do with performing what amounts to a cast?"

I want to emphasize that I'm not complaining just to complain, nor placing blame on any specific party. I'm just "reporting a bug" in my learning experience with the language, and trying to provide as much info as possible :)

> I want to emphasize that I'm not complaining just to complain,

Oh yeah totally! It is very helpful.

> It could just be that during my learning stage I learned

I mean, I think this is very reasonable and intentional. Trait objects are a pretty niche feature of Rust already, and non-owned trait objects are even more niche than that. The book does guide you towards Box<dyn Trait> for this reason.

> or the String thing (really, the Deref thing), further down others weigh in with a case where it doesn't work as expected,

Yeah so the trick here is a balance between not wanting coercion willy-nilly, and also making some cases work well. You had cited method calls specifically, and those should work due to auto-ref/deref. The example given isn't about method calls, it's about match not doing Deref coercion. That being said it's really easy to assume that it always does it, because it does do it in the right places most of the time! There's interesting tradeoffs here...

> The book does guide you towards Box<dyn Trait> for this reason.

Yeah. And that makes sense, though an aside that explains the broader concept ("Note: dyn is usually paired with Box, but it can be used to describe any reference") would help establish the more generalized understanding (it is possible this aside already exists and I just missed it)

Re: the deref coercion, I do think many cases of this class of problem I'm describing come down to implicit behavior the compiler does to infer certain commonly-used and onerous syntactical elements, to make code cleaner and easier to write. I understand why this was deemed necessary, and it's not as much a problem as "magical" behavior in other technologies, because (seemingly) everything the compiler does implicitly maps directly to an equivalent explicit version.

But it leads to a lot of confusion when those rails eventually break. In terms of brevity, this can be looked at as "gracefully degrading": it makes the normal cases better, and reverts to the "baseline" behavior when you step outside of those. But in the context of learning, it is not such a strict win, because the implicit cases have colored the user's understanding of the language itself, actively hampering their ability to venture off the golden path (or form generalizations about concepts).

I kind of wish the compiler had a "turn off all implicit behavior" option, so that you could learn how everything is done explicitly before turning the helpers back on for the sake of productivity.

> (it is possible this aside already exists and I just missed it)

Yeah I am not sure if it does or not; I'll make a mental note to go check sometime.

> I kind of wish the compiler had a "turn off all implicit behavior" option,

The problem is that what is "implicit" is different for everyone. Some people would say that Box::new is "implicit" because you don't see the malloc. Some might even say that malloc is implicit because you don't see the sbrk/memmap!

> But in the context of learning, it is not such a strict win,

Yeah so it's tricky! The thing is, without some way to jumpstart knowledge, people may never even learn in the first place. Like, by this argument, everyone should start with physics, because well, C compiles to asm complies to machine code which is actually just code for a chipset. There is no "I know what everything is doing" starting point. In reality, people jump in, start somewhere, and then expand what they know from there. This is true on every topic in every field. We still teach kids Newtonian physics even though on some level it's "wrong," you know?