←back to thread

177 points signa11 | 2 comments | | HN request time: 0.723s | source
Show context
sfink ◴[] No.42162416[source]
I'm a relative beginner at Rust, but this matches my experience fairly well. Especially the part about the brittleness, where adding just one little thing can require propagating changes throughout a project. It might be adding lifetimes, or switching between values and references, or wrapping things in Rc or Arc or RefCell or Box or something. It seems hard to do Rust in a fully bottom-up fashion; you'll end up having to adjusting all the pieces repeatedly as you fit them together.

Maybe there's a style I haven't learned yet where you start out with Arc everywhere, or Rc, or Arc<Mutex<T>>, or whatever, and get everything working first then strip out as many wrappers as possible? It just feels wrong to go around hiding everything that's going on from the borrow checker and moving the errors to runtime. But maybe that's what you need to do to prototype things in Rust without a lot of pain? I don't know enough to know.

I have already noticed that building up the mindset of figuring out your ownership story, where your values are being moved to all the time, is addictive and contagious -- I'm sneaking more and more Rusty ways of doing things into my C++ code, and so far it feels beneficial.

replies(3): >>42164716 #>>42165098 #>>42165223 #
IshKebab ◴[] No.42165223[source]
I don't think this is really unique to Rust. Any language that requires you to write stuff down about interfaces might require you to change them in a lot of places if you get it wrong. E.g. if you get the type of a parameter that is passed everywhere wrong.

I guess the hard bit here is it's more difficult to make a mental model of the borrow checker than it is most other features, and also the fact that borrow checking is a late phase of the compiler so you might put in a fair amount of work before getting feedback.

replies(1): >>42165604 #
sfink ◴[] No.42165604[source]
> I don't think this is really unique to Rust. Any language that requires you to write stuff down about interfaces might require you to change them in a lot of places if you get it wrong. E.g. if you get the type of a parameter that is passed everywhere wrong.

My experience doesn't match this. I think the difference is that Rust has... "specific types", to pick an arbitrary term. The types store more information, which means that there are many more ways that you might need to update an interface.

An interface with a function that takes a C++ pointer doesn't imply anything other than that the pointer is valid at the beginning of the calthe return value of l to that function. It doesn't even tell you whether it can be nullptr or not. It might be a pointer extracted from a `std::unique_ptr`, in which case the actual expectation is that the pointer be valid when the function returns. Or it might be passed the return value of `new`, in which case the expectation is that the pointer be either valid but now owned by something else, or invalid when the function returns. And you might change from one expectation to the other merely by removing a `delete` call 7 levels deep, without needing to adjust any of the intervening layers.

Rust, on the other hand, encodes a specific subset of the possible lifetimes that the C++ version accepts, and every intervening layer has to agree on that lifetime or at least the structure of it.

And it's not just lifetimes. In Rust, you'd typically make everything a specific type -- maybe an enum or Option or whatever. In C++, I often find myself intentionally degenerating even (C++) enums to ints when I need to store or output or manipulate them -- I want to test `val < FirstCustomValue` or something. A parameter that started out as an enum with 3 variants can add on a couple of orthogonal bits without perturbing much of anything.

The more specific the types used in practice, the more they'll need to be adjusted to accommodate changes. C++ doesn't even have the facilities for specializing the types as precisely, but more importantly types are typically left pretty loose in practice -- at great cost to stability and correctness, but with resulting benefit to adaptability.

A lump of mud is much more adaptable than a carved sculpture.

replies(1): >>42165948 #
1. IshKebab ◴[] No.42165948[source]
It sounds like you're agreeing with my point to be honest. The problem isn't Rust or the borrow checker; it's simply checking lots of things on interfaces. The more you check, the more you'll have to change when you get it wrong.

Rust happens to check quite a lot. I imagine it is similar for things like monads in Haskell, or probably formal languages like Lean and Dafny... but I have no experience of that so it's just a guess.

replies(1): >>42166574 #
2. sfink ◴[] No.42166574[source]
No, I'm not agreeing with you. You're agreeing with me.

Wait a minute... :-)

Sorry, somehow I managed to quote the portion of your message that I was agreeing with. You just said it more concisely than I could.

The aspect I disagree with is whether brittleness to change is more due to the complexity of the borrow checker vs interfaces with very constrained types. I mean, the complexity of the borrow checker definitely factors into it, especially with how late it runs (as you said) and the fact that it's all or nothing, which makes it hard to change things incrementally.

But (imho) the borrow checker is less complex than, say, the rules around C++ constructor variants. And RVO/NRVO. And name lookup. And definitely less complex than undefined behavior. I'd rather memorize the borrow checker than those. They contribute to some amount of brittleness to change, especially when monkeying with move constructors, but in practice it seems far less than Rust. I really think it's more about the specificity of types in the language.

I should probably mention that I still think it's a good tradeoff for many of the types of programs I write, but it is a tradeoff.

Anyway, this is all more just a realization that you sparked in me. It's ok if we, uh, disagree to agree.