Why do all docs say RefCell is bad?

I must apologize. I basically read this:

and then didn't read one sentence further

and instead just wrote my comment. My bad, really.

4 Likes

Leaving interior mutability aside for a moment (I explained above why the Box wouldnt work btw, pls correct me if I am wrong), another question - lets say I have a data structure with nodes referring to other nodes etc.. - no interior mutability, just immutable references. So in this case what does Rust recommend - should I use a pointer & reference (and deal with all the lifetime declarations and all that) or use an Rc ?

From my (kind of limited) understanding so far, I see that references and lifetimes make sense when you are dealing with a function call chain where ppl are passing around references here and there. But if we are talking about a data structure thats gonna outlive a function call chain and be around for a long time, an Rc<> makes more sense than an & reference. Again pls let me know what the Rust recommendation is in this case

In one sense I agree, but in another sense I disagree. Arc<Mutex<...>> can serve a different function than Rc<RefCell<...>>, which is to enable communication between threads. And communication between threads is valuable.

2 Likes

This is the main problem with using toy examples to illustrate architecture problems -- a toy example can't capture all the requirements of your real-world problem. It's the constraints of the real world that will determine what architecture you should use. General design principles like "avoid shared mutability" must take a back seat.

It's not clear to me whether this example is meant to be a trimmed-down version of a real problem, or whether it's something you cooked up specifically to illustrate use of RefCell. Of course you can come up with examples where RefCell is the exact right solution; that's why it exists, after all. But there might be just as many examples of problems where RefCell solves the immediate problem, but suboptimally, and it would be better to restructure the data. This is the kind of thing where you can't always tell just from looking at a trimmed-down example because the part that needs to be restructured is probably in the part you trimmed out.

The issue with this question is that there's no one answer -- the best solution for a real-world problem depends on its real-world constraints. So, given sharing but no mutability, you could use Rc, or you could use a vector with indices, or an arena with references. You could even use raw pointers! Rust doesn't recommend anything, it allows you to do whatever is most appropriate for your problem. What's that? Well, the problem statement is far too broad to make a blanket recommendation.

Personally, I think we (the Rust community) sometimes put too much emphasis on being "idiomatic", occasionally even replacing good, performant, obvious code with tricky, slow, buggy code, because it uses more iterators or whatever. Sometimes this happens because we make a recommendation based on local reasoning, but there are nonlocal effects we don't know about. You have to judge whether the advice you get on a forum like this is good or not. We can make suggestions, explore alternatives, and pronounce general principles, but at the end of the day you have to decide what works best in your project. Sometimes what works best is RefCell, and if so, there's no reason to avoid using it.

6 Likes

Thx for the detailed response. The toy example of two nodes N1 and N2 both referring to N3 mutably, is trimmed down from a real use case, and I think its quite a common use case in any large piece of system software.

As a general feedback on documentation: Rust has awesome documentation, thanks to which there is no frustration about lack of information! What I would love to see as an addition to it or an alternate way of documentation is describing real world architectural issues people face in C/C++ and giving example of how that will be done in Rust. So I have been a systems programmer for 20 yrs now working on C and extensively dealing with every single kind of issue that rust is trying to address. So the problems itself are familiar to me - so for me, if I get a "architecture using rust" kind of a cookbook describing real world architectures, that would have really helped me speed up my learning curve in understanding what each concept is really meant to do. The current documentations go at it from the other way round - it talks theory and concepts first like mutability, shared mutability, aliasing etc.. and then shows what to do in rust for each of those. So for me it took me a while to prepare a mental map that "ok this particular thing called mutable xyz is actually something to be used in this kind of architecture that I am familiar with". I wanted a reverse documentation, where we talk about the big problems, show the rust-way of solving it, and that automatically teaches me all these basic concepts. Anyways, once I get more familiar with rust and have more code under my belt, ill pbbly try writing some documentation myself like that

7 Likes

A suggested initial outline for your "reverse documentation" cookbook might get other Rustaceans to start contributing. I suspect it wouldn't take long to assemble a sizable corpus of such examples, at least at the level of textual descriptions. (Adding actual correct code to the examples will tend to take somewhat longer.)

The biggest obstacle I foresee is that real-world problems often are complex, whereas these cookbook snippets inherently will tend to be relatively simple, focussing on only a very few issues per example.

3 Likes

Yep, requests for a "Rust for C++ devs" or a "design patterns cookbook" have come up several times.

https://github.com/nrc/r4cppp is the most complete project I'm aware of in this space. We could probably benefit from a more attempts to tackle it from different angles.

3 Likes

I did not know about that, thanks for the pointer. Let me read through it and see what else can be added there

Probably you're reading too much into that. The sentence above ^^ doesn't mean "bad".

Sometimes one needs a RefCell, it does exist for a reason. But when learning Rust, beginners often reach for tools to imitate patterns in other languages (e.g. interior mutability) because they don't yet understand how to write something in a nicer, more idiomatic, less work-around-ish way. That's why the docs of last-resort tools need to say that "this is a last resort tool, use it only if you are sure you need it".

It has to be communicated somehow.

4 Likes

The fundamental problem with this code is the use of reference counting, which causes you to need interior mutability, which causes you to need runtime checks, and opens up the possibility of runtime panics or deadlocks that we wish were caught at compile time.

If you were to forgo reference counting you could implement your graph without fewer and less risky runtime checks and more bugs caught at compile time. In this case you have nodes that are owned the graph, but there compiler doesn't know that, and that is why you need interior mutability.

To avoid interior mutability, you need to have a single owner of the nodes, something like:

struct Graph {
  all_nodes: Vec<Node>, // or Vec<Box<Node>>
  root: usize, // or root is always 0
}

struct Node {
  children: HashSet<usize>, // or tinyset::SetUsize
}

You can expose the same API, but now the only runtime checks are that your indices are within the array bounds. And those checks will only fail if you use an index that didn't come from the array.

5 Likes

I have no sensible suggestions here but that statement struck me as being a bit odd.

I like to think there is no data structure that outlives a call chain. All call chains start from main(). Ergo main() or some function down some chain has created that data and is responsible for it. Perhaps passing that responsibility elsewhere,

A data structure that outlives a call chain is called a memory leak!

Or did I not understand you correctly?

2 Likes

Rc<RefCell> is like duct tape.

It's very versatile, and can fix a multitude of problems in a pinch. For some problems, it's even the best thing to use. But if the thing you're building is more than about 10% wrapped in duct tape, you might want to reconsider your design process!

17 Likes

Yup exactly. After all the discussions yday and because of some inherent impression deeply ingrained by now that "refcell is evil" :), I spent some time thinking about how to avoid it, and eventually I did EXACTLY what you have suggested here. And I have to say yes, it does look a LOT cleaner than before. So thanks everyone for all the thought provoking discussions, suggestions and pointers!

5 Likes

By the way, there is interior mutability and interior mutability.

That is, Rc<RefCell<Node>> can sometimes be replaced by Rc<NodeWithCellFields>, which has the advantage of offering zero-cost interior mutability.

Here is, for instance, a naive implementation of a doubly linked list not using RefCell but just Cell:

2 Likes

Or did the discussions provoke fewer pointers?

5 Likes

I did not read through all 25 response -- apologies if this has already been mentioned.

One thing that has pushed me to avoid RefCell whenever possible is that on wasm32, debugging double mut borrows is nearly impossible to the point I'm often git reverting and bisecting to see what change cause the error.

This doesn't imply that RefCell is bad, only that my usage of it is bad -- but the debugging frustrations have often caused rewrite to massively reduce usage of RefCell.

Slightly off topic for this thread, but if the issue you're having with debugging is the lack of panic messages/stack traces, you should look into console_error_panic_hook.

4 Likes

On a side note, if you're not implementing a graph just for the hell of it, you might want to search for a crate that has already implemented it.

1 Like

(for example petgraph)

1 Like

I actually think that Rc<RefCell> is more like a nail and Rust is a screwdriver.

Nails aren't bad. They have advantages and disadvantages. There are a lot of instances where you can build the same thing using screws or nails. Sometimes nails are the best solution. But it's not fun to hammer a nail in with a screwdriver.

5 Likes