That's a perfectly reasonable conclusion--and callback patterns are definitely, by far, the single case where Rust's restrictions are the most onerous. In most other cases, where closures are passed for more temporary use, these restrictions don't matter nearly as much, fortunately, or else they might not even be part of the language. And in a few cases (like automatic parallelization) the restrictions are actually incredibly helpful (since if a closure's environment can't be written to while shared, it's generally safe to execute in another thread!). So, whether Rust is a good fit for your project or not really depends on your usecase.
I've never dabbled with multi-threading in Rust, so I'm curious how far it can go in practice.
In our closure, we probably can't have any RefCell or Rc or anything that could reach either of those things, because those can't be safely reachable by multiple (even read-only) threads.
And I think these closures can't contain references to the outside world? I might be wrong on this one. And maybe structured concurrency could help here, though RefCell and Rc might confound that.
How well do Rust closures work in practice? Have you found a good use for them?
> I've never dabbled with multi-threading in Rust, so I'm curious how far it can go in practice.
Extremely far. The strict mutability rules are basically your downpayment for the easiest safe parallelism of any production language (that isn't purely functional, anyway). In particular the Rayon library (https://docs.rs/rayon/1.5.1/rayon/index.html) allows you to make your sequential code parallel by merely changing `.iter()` to `.par_iter()`, and experimentally offers comparable performance to best in class work stealing schedulers, often outperforming manual C and C++ implementations of the same algorithms.
> In our closure, we probably can't have any RefCell or Rc or anything that could reach either of those things, because those can't be safely reachable by multiple (even read-only) threads.
Assuming you're talking about Vale, I'm curious how you actually enforce this in practice. `Cell<T>` is basically an "overhead free" type in a single-threaded context, but it's not safe to access from multiple threads since it lets you mutate through a shared reference. To get around that, you either need to have a different type for shared references from other threads, or you need something like GhostCell, which separates permission to read/write to the Cell from access to the Cell itself. I would have to learn more about how your language works to say more.
If you're talking about in Rust, yes, you're correct. Using `Cell` or `RefCell` prevents use with automatic parallelization APIs, which is one big reason they aren't just done automatically for you (as they effectively are in many other languages). However, there are thread-safe mutability types with various tradeoffs: atomics in specialized cases where the type has native atomic operations, mutexes and reader-writer locks for thread safe concurrent mutable access, and APIs like GhostCell that use type-level trickery to make access safe without overhead, at the cost of a more complex API (something like GhostCell probably has the best performance tradeoffs for the UI case, but it is not very easy to use!). For `Rc`, your option is more straightforward: just use `Arc` and you are thread safe again (assuming the type you're protecting is).
> And I think these closures can't contain references to the outside world? I might be wrong on this one. And maybe structured concurrency could help here, though RefCell and Rc might confound that.
Rust closures can contain references to the outside world--and usually do! The references are just heavily restricted, as the text notes, to the point that it's hard to use them for callbacks (it's still possible, but there's a lot of ceremony).
> How well do Rust closures work in practice? Have you found a good use for them?
Hm, apparently I should have explained this up front, since apparently I did a bit too good a job explaining the downsides! Closures are used very heavily in Rust, almost everywhere in fact. They're a cornerstone of the Rust iterator API (probably the most commonly used trait in all of Rust), Rayon's parallelization API, numerous methods on Option, Result, etc., and are exploited by tons and tons of functions. Closures are very restricted, but you also get a lot as a result--they not only parallelize well, they are zero overhead abstractions that frequently inline to the same thing as hand-rolled SIMD (as noted here: https://twitter.com/badamczewski01/status/138634513508098458...) which means people are never worried about performance overhead when including them in abstractions. They can also be boxed, reference counted, etc. to erase their types in cases where you're okay with paying for dynamic dispatch (in which case they more closely resemble closures in typical languages).
So, they work extremely well in practice! What don't work well in practice in Rust are "callback registration" patterns in general. In fact, this is so much the case that I'd say finding a way to do things without callbacks is the main reason people need to refactor their applications for Rust.
> So, whether Rust is a good fit for your project or not really depends on your usecase.
One of the biggest problems in software engineering seems to be that requirements change all the time. So unfortunately I can't know beforehand whether Rust fits my usecase unless the scope of the project is very limited. I do like the safety guarantees of Rust but now feel that they might conflict too much with flexibility at some point.