[Idea] Improving the ergonomics when using Rc

I don’t find this argument very strong and potentially hazardous. Rc<T> will always be a visibly heavyweight type and have ergonomics implications (e.g. only immutable sharing). I also see patterns were people avoid Rc/Arc for the reason that refactoring to them is too hard and too much of an effort.

Appeals to “making things too easy for people” always strike me as odd. Bad usability for the sake of keeping people away from things puts no trust in peoples ability to choose their approaches. Community practice is the key here and it evolves.

There was a time in Rust (around 1.0) where everyone wanted to solve things using huge borrowing constructs, while nowadays, ownership tricks are beginning to get more popular. This is normal and not steered by the project team making things harder or easier, but by how the system is constructed.

Also, there’s a not-so-small group of people that does feel like Rc and Arc are generally underused in many systems.

3 Likes

The exact same argument can be made about the opposite point. For instance, I don’t find the “it’s so annoying to write rc.clone() that we should make it implicit and/or add it to the core language” argument strong at all, at least not strong enough to warrant a language change in any case.

Furthermore, the argument is not that Rc “should have bad usability”. Rather, it is that one of the things it does (cloning) shouldn’t be actively encouraged to be treated as insignificant and become invisible, especially not by means that are known to have caused countless problems and correctness bugs in the history of programming language design.

I don’t believe anyone who is against this proposal wants to punish fellow programmers for using Rc. I personally think that it’s one of the best and most important ideas in the art of memory management. I have written lots of code (heavy on graph manipulation) using Rc all the time, and I wouldn’t discourage anyone from using it should they deem it a good solution. I would just like to be able to easily read that code please. And typing a single method call is not a huge price to pay in exchange.

9 Likes

Very well said, thank you; this is exactly my concern throughout this thread.

4 Likes

Just a random thought that could (or not) conciliate both “parties” : now that the language is mature and far away from its overly sigilesque beginnings, could it be considered to add a cloning sigil / prefix-or-suffix operator ?

More precisely, I’d imagine an ops::CheapClone : Clone empty/marker trait that would allow using such a sugared operator on types like Rc or Arc

1 Like

I am generally of the opinion that AutoClone is not the deadly threat to all the things we know and love about rust that some people are making it out to be; but surely others have other already stated the same reasons I would have, so I won’t bore you.


On the other hand, here’s one thing that I know isn’t mentioned in the thread: (I searched!)

impl<T: Clone> Rc<T> {
    /// Makes a mutable reference into the given `Rc`.
    ///
    /// If there are other `Rc` or [`Weak`][weak] pointers to the same value,
    /// then `make_mut` will invoke [`clone`][clone] on the inner value to
    /// ensure unique ownership. This is also referred to as clone-on-write.
    #[inline]
    #[stable(feature = "rc_unique", since = "1.4.0")]
    pub fn make_mut(this: &mut Self) -> &mut T {
        ...
    }
}

Thanks to make_mut, AutoClone on Rcs and Arcs could create drastic performance and memory issues in existing, working code. (like this issue) And it wouldn’t be easy to fix, either!

So… I don’t know if I’d implement it for these types!

6 Likes

I already assume deref() and as_ref() are always super cheap and nothing else than a pointer cast/offset. I would be unpleasantly surprised if someone actually took advantage of ability to run expensive arbitrary code in these contexts.

I would not like to go back there, so no, let’s not add more operators for something as trivial as one argument-less method call. In this regard .clone() is no better or worse candidate for an operator than e.g. .into() was for postfix ! a couple of months ago — there is really not much value in either.

2 Likes

In Swift I don’t know when refcounts are optimized out and when they aren’t. This feels similar to the problem I have with Rust: I don’t know when bounds checks are optimized out and when they aren’t.

If Rust had a practical solution for this “is it optimized out or not” problem, then automatic refcounts and other automatic things could be OK. Otherwise I prefer erring on the side of visibility and control.

What about automutex and autorefcell? These are often used together with the refcount types, so autorefcount solves only half of the boilerplate.

1 Like

I would like to propose a somewhat alternative mechanism of achieving the same goal.

An attribute that be added to a crate/module/function which explicitly lists what types should be implicitly cloned in the context of that attribute. i.e. #![AutoClone(Arc, Rc)]

There’s no ImplicitClone trait, and nothing is declared on the type itself. This allows the caller to decide what they view as expensive or cheap, rather then the struct author deciding - because different consumers have different opinions here.

There’s an explicit list of types which are being opted in in the calling scope, which means you don’t need to worry that other types will implicitly be cloned which may be expensive. The crate author can decide explicitly, and their decisions don’t leak out into their dependees.

This doesn’t bifurcate the language; you have to be explicit about what you’re applying this to, and as there’s nothing special on the type-defining side, you don’t need to worry about half of the ecosystem not using a marker trait.

Also, if someone decides they want to undo this change in their crate, it should be possible to write some code which would re-write the source to remove the attribute and make the implicit clones explicit again.

My particular motivation here is that I use rust in higher-level contexts with a lot of Futures, and absent async/await with borrowing, I have to clone a lot of Arcs into Futures. I personally don’t care about the cost of these Arc clones (they’re much cheaper than the network calls / forks that run in my particular Futures), and I don’t care too much about having to write them, but the number of them I need to read actually makes it harder to read the intent of the code. In these projects, I would happily mark Arc as AutoClone, but I can see why other contexts would find this problematic. So let’s allow the author to decide, in a way that doesn’t force the decision on others?

3 Likes

Please remember that usability and ergonomics depends not just on writing code and reading your own code, but on reading other people’s code. Opt-ins don’t solve that problem; they make it worse by making code inconsistent.

.clone() is self-documenting and makes it clear the points at which a clone occurs. That’s not a property I’d like to lose.

3 Likes

I can make the same argument about Copy - sometimes it’s expensive, and a reader may want to be altered to a copy happening. So let’s make Copy also explicit (need to call .copy()), and also add an AutoCopy attribute. We could do this in the next edition.

Perhaps we could add #[AutoCopy(u8, i8, ...)] to the prelude for convenience. Perhaps we shouldn’t because explicit is better than implicit, and each crate should opt in to their own idea of what’s cheap to copy.

Then we would have more consistency, and more explicitness, with a well known place to look for behaviour changes.

2 Likes

There are already plans to flag and warn about expensive copies. Also, it’s not just a matter of “expensive”, it’s a matter of user-defined code (which can do anything) versus entirely compiler-generated code.

7 Likes

You are right that reading code is important too, but I think writing .clone is not the only way and may not the best way to help that. Those lightweight clone actually are noise make expense clone difficult to locate. With IDE and RLS’s help, we can highlight where autoclone occurs too. We also can have rustfix to covert autoclone into explicit clone if the crate author want to.

We can do so to flag and warn autoclone also if it exists. And I don’t think “compiler genrated code” is strong reason to against introduce autoclone, as we already have operator overload and Deref which can run arbitrary user code. And most importantly, I believe compiler is a tool to execute user code after all.

I think explicitness is generally good and should encouraged but still some can be elided. The crate author should have a choice to opt-in, currently, is not possible.

3 Likes

Apologies if I missed this up thread, but what about requiring that the clone function for AutoClone must be const? IIRC @cramertj proposed this recently. If you can’t do I/O, and you can only perform intermediate allocations that don’t escape the function, then many of the potential perf implications of pervasive AutoClone could be significantly mitigated. Going even further, you could imagine a compiler lint that ensures it doesn’t allocate temporarily even.

That assumes that const fn will never be able to make escaping allocations. That may not be coming anytime soon but it would certainly be useful so I wouldn’t rule it out yet.

3 Likes

Only skimmed the discussion here, so I apologize if I’m saying something that hasn’t been said before.

In Rc’s case, the clone() is particularly annoying because Rc can be used in situations where you intentionally want to handwave memory management. Eg because the particular part of the code you’re working in is high-level code where performance issues are going to be much more dramatic than an indirect pointer access or a cache miss, and the Rc clone()s could lead to you missing the forest for the trees. Or a “penny-wise, pound-foolish” style of programming.

The best argument I can think of against it is “that’s not the niche Rust is supposed to occupy, you should use a different language”. But even assuming that’s an option, that means you lose all the expressiveness of the type system and have to re-expose that in the other language.

I unfortunately can’t think of a good solution here. To me, it’s somewhat distinct from Copy and Clone. Lumping a reference count increment in with clone seems like an unfair grouping. It probably isn’t as cheap as a small Copy, but in all but the most tight loops, it’s probably going to be insignificant compared to any other clone(). But beyond the potential backwards compatibility issues, adding another trait seems risking trait pollution.

EDIT: Continued reading a bit more and I so far tenatively like @illicitonion 's solution. This seems like a decent way of labeling a high-level scope as such so that you can tell that any clone()s involved are actually significant and worthy of note. Eg doing syscalls or copying a substantial amount of data.

2 Likes

My assumption is that if const fn grows the ability to return allocations we’ll also have sufficient compiler technology available to enforce a no-allocation requirement for AutoClone impls. Is that reasonable?

Even beyond Rc though, sometimes it would be better if String/&str/Cow<str> were all collapsed into one type which you could split/append/copy willy-nilly, like in (for instance) Python. On the other hand there are cases where performance matters and even simple copies are relevant.

This is even more general than just Clone though. Sometimes I’d like panics/aborts to be explicit so I can do things like (eg.) catch memory allocation errors. Other times even Result/? is just code-noise and I’d prefer to have silent exceptions.

It seems like what we really need is a heirarchy of languages which make the explicitness/expressiveness trade-off differently, the ability to intermix them and the ability to type-check across them. Kinda like how we translate Rust down to MIR, except if MIR retained all the type-safety of Rust, and also another language in the opposite direction which has implicit memory management and compiles to Rust.

2 Likes

That’s a good idea- though I’m not sure what such a design would look like or how we might be future compatible with it. Probably worth coming up with some ideas as part of the design of AutoClone.

On the other hand I worry that any attempt to enforce that requirement would balloon into some kind of allocation-as-an-effect system, way overshooting the complexity budget.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.