#AskFedi: does anyone have a good article that explains *in depth* what Cell and RefCell are for in #Rust? So not just "this is what they do" or "this is how they work", but more questions like "why does this exist", "why would you need this", "what other approaches could have been taken and why was this one better".
(Do not send me ad-hoc explanations please; an explanation that fits into a toot is almost certainly not going to be in-depth enough here. I'm looking for articles that were written with deliberation and review.)
@joepie91 this paper about an alternative called GhostCell has a bit of background about the problem that the various cells are trying to solve: https://plv.mpi-sws.org/rustbelt/ghostcell/paper.pdf
@dcreager That seems to have been the most useful one so far, thanks! It does seem to explain part of what I'm wondering about, but I'm having to make some inferences here as it mostly talks about Mutex/RwLock - do Cell and RefCell exist for the same reason of "doing runtime instead of compile-time checking"? And if so, and they are just not thread-safe, why would you ever use the not-thread-safe ones?
@joepie91 @dcreager I believe the answer is speed. If you have a situation where multiple parts of your code need to be able to write to a value, but they're all on the same thread, you don't need to do the relatively expensive atomic locking operations that Mutex and RwLock do.
Ime they're relatively niche. I almost never use Cell or RefCell in my code.
@joepie91 @dcreager there are situations where you don't need thread safety and still want multiple references to something potentially mutable. Keep in mind that Rust is strongly performance oriented.
Also, literally the most common use case for those types are actually Mutex and RwLock. They internally hold the value in UnsafeCell type to be able to work as intended. (But I recently wrote a Mutex implementation for single theaded async code, which works with RefCell and just yields when cell can't be borrowed)
I also often see it in a singleton.
> do Cell and RefCell exist for the same reason of "doing runtime instead of compile-time checking"?
That's why RefCell exists. Mutex/RwLock will _wait_ until it's your turn to get (exclusive) mut access. RefCell is single-threaded, so waiting doesn't make sense—there's no other thread that could make progress while you're waiting. For RefCell, taking >1 mut refs is a logic error, since the other mut ref has to be higher up your call stack, and you could have just passed that down!
> And if so, and they are just not thread-safe, why would you ever use the not-thread-safe ones?
Mutex/RwLock have to synchronize multiple threads, which requires atomic operations that aren't free. If you know you're only working in one thread, RefCell will be faster since it doesn't have to use atomic ops to query/update the "who's using this" state.
> do Cell and RefCell exist for the same reason of "doing runtime instead of compile-time checking"?
Cell is different — Cell actually lets you get rid of the checks completely! RefCell is for "mutate something in place". Cell is for "pull something out into scratch space, update it there, and put it back". The "pull it out"/"put it back" parts don't have to worry about other threads, and always leave _something_ valid in the cell, so everything is kosher to the borrow checker.
@joepie91 https://arxiv.org/abs/2405.08372 2.1 and following the graph of people trying to control interior mutability concepts I assume?
@raito Hmm, this seems a bit too dense/abstract for me - it seems to be *about* interior mutability, but doesn't seem to explain how eg. Rust ended on the Cell/RefCell abstractions, or exactly what the difference is with your standard mutable/immutable references.
The problem I keep running into is that everyone says "you can't mutate a thing to which you have a shared reference, for safety reasons, but if you put it in a Cell/RefCell then you can" and it is entirely unclear to me why it's suddenly okay in a Cell/RefCell and how it ensures safety in that case, and if it's so easy, why that isn't just default behaviour for references, and so on.
@joepie91 maybe i can really just explain it in a toot but you let me know if it's not sufficient
Ref/RefCell is a wrapper structure that switches the static borrow checking to a *dynamic* borrow checking
The reason for why you can is because Cell/RefCell is just a proxy for your reference and the borrow checker says "OK you can do that", under the hood, it uses `unsafe` to bypass safe Rust rules.
But *also*, it is still OK because it's full of dynamic checking code.
@joepie91 We often say that Ref/RefCell implements a dynamic borrow checker *at runtime*, as Rust wants to be ~zero cost abstraction-ish, you cannot have it as the default, it's too expensive.
@raito Okay, I think *that* is the key point I was looking for. But this still leaves the question: if there's Cell/RefCell and there's also RwLock/Mutex, which seem to exist for the same purpose, why would you be using one or the other?
Cell/RefCell: single-threaded
RwLock/Mutex: multi-threaded
RwLock/Mutex is more expensive than Cell/RefCell!
@joepie91 (note that you can reason about the Sync/Send traits to conclude this as well, they do not give rise to the same Sync/Send traits depending on whether you do Rc<RefCell<T>> or Arc<RefCell<T>>, etc. Some are non-sense, some makes sense.)
@joepie91 Not ideal, but this has some of the explanation. https://blog.iany.me/2019/02/rust-cell-and-refcell/
@modulux@node.isonomia.net Yep, I did read that, but unfortunately it doesn't really talk about the "why does it exist", so I'm having a lot of trouble understanding it in context - and I don't really want to just "add it whenever the compiler complains" either, I need a mental model to reason about where it's appropriate, basically.
@modulux Yep, I did read that, but unfortunately it doesn't really talk about the "why does it exist", so I'm having a lot of trouble understanding it in context - and I don't really want to just "add it whenever the compiler complains" either, I need a mental model to reason about where it's appropriate, basically.
@joepie91 Right, it just has a bit of historical background of why it was introduced. The other resource I have on it is this one, neither is, I think, very complete. https://badboi.dev/rust/2020/07/17/cell-refcell.html
@modulux Oh, this one looks closer to what I'm looking for at a glance - will give it a read, thanks 🙂
@modulux Yep, this ended up being it! Thanks again!
@joepie91 The std::cell API docs are pretty good too: https://doc.rust-lang.org/std/cell/
(I am aware of the Rust book. It doesn't click with me.)