Nico’s Handle trait confuses me

First, Nico's Handle blog post suggested the Handle trait should signify whether or not a clone aliases the same data, especially for types with interior mutability, but then his Handle trait wound up being save.

I'd have expected that both Handle and !Handle would enable some unsafe compiler optimisations, so maybe this trait:

unsafe trait HandleIsKnown : Deref + Clone {
    const is_handle: bool;

    final fn handle(&self) -> Self
    where Self::is_handle == true
    { self.clone() }
}

It’d always be unsafe to say if a type is a handle or not a handle, but either way enables some optimisations for what it dereferences to, and miss-use gives segfaults. If a handle, then you get the handle method, which avoids Arc::clone.

Second, there is a mildly serious risk that Handle gets miss-used as merely “ergonomic clone”, which seemingly precludes its usage for aliasing information.

Around this, we already have serious logic bugs arise when passing around Iterator<Item = &mut T> with T: Copy lots, because existing ergonomics tricks often some local T to be mutated and discarded, instead of the &mut T, so more implicit behavior here sounds pretty fraught.

I’d suggest that “clone ergonomics” should be a completely orthogonal topic, unrelated to aliasing and some Handle trait, if only to prevent abuse.

As a "sane default proposal", each & every source file could declare the non-copy types it wishes to be auto clone, and declare the copy types it wants to not be auto clone:

#![implicit_clone(Rc,Arc<Foo>,GuiPtr,GamePtr)]

#[no_implicit_copy(&[u64, N], N>1)]   // prevent local copy bugs
fn foo(.. x: impl Iterator<Item = &mut [u64, 2]>, ..) { ..

There could be a lint against #[implicit_clone(T)] with T: Clone + !Handle, but importantly you could turn this lint off if you want implicit cloning some some !Handle type. Allowing the automation of regular clones would hopefully prevents the incorrect proliferation of impl Handle on non-pointer types for purely ergonomic concerns.

Implicit clones would never occur for fully generic types T: ..traits.., maybe even T: Copy could stop being implicit clone eventually. Implicit clones do occur when wrapped inside some pointer type though, so like Rc<T> types. It's likely that folks who want implicit clones want them only in clone heavy buisness logic, but do not want them in really performance sensitive code, like custom executors.

1 Like

What specific optimizations are you thinking of?

1 Like

I always thought that the article was Work in Progress (WIP) especially the More to come! part of the article's conclusion. I'd wait for the article to finish what it meant to say before jumping my guns.

3 Likes

I did not get the impression that any new optimizations are planned here. I think this is all about ergonomics and library-level expectations, with some syntactic support from the language, but without opsem involvement.

11 Likes

If that's truly the case, and you probably know better then me, I am as perplexed as the OP. What would Handle even get you?

1 Like

Primarily, the trait gives the benefit that the old recommendation to write Arc::clone(&value) does — it indicates that the actual value that you're manipulating behind the handle is the same object both in the original binding and any clones.

The second is enabling some sort of syntactically transparent cloning capture, the exact proposal for which hasn't been posted about yet for the desire to split the blog effort into digestible independent chunks.

There's no operational meaning to Handle by itself. But there is semantic benefit to identifying the subset of clones which are handle clones, even if the language doesn't utilize that semantic information.

It's similar reason to why we have nominal trait implementation instead of just structural protocol conformance.

6 Likes

And which clones are handle clones? Should struct Ctx { a: Arc<A>, b: Arc<C>,....., z: Arc<Z> } impl Handle?

And then, Handle doesn't even fully solve the Arc::clone(&x) thing, since you might very well have Arc<T: Handle> and now we're back to the "wait, which one am I cloning" confusion.

3 Likes

This whole handle thing seems to be of limited value to me.

For a start i don't want it to ever be implicit, either in my code or in any dependency. As I described recently over on reddit I recently found that 27 % of my total runtime was spent on Arc reference counting. I rewrote the code to use borrowing for a massive speedup. (More details in the original thread, not going to repeat that here.)

I don't think hidden reference counting increments/decrements is ever a good idea. And if we are explicit about it, what difference does handle() vs clone() make?

Implicit copying is already bad enough. Yes, copying a u8 is obviously fine. Copying a [u8; 4096] is bad, and should never have been Copy in Rust. Where is the line? Depends on architecture and program. So I would be inclined to say that no arrays should ever have been Copy.

Handle will just lead to more such ambiguity and bad decisions.

1 Like

I don't think hidden reference counting increments/decrements is ever a good idea.

I somewhat disagree with this: being able to cut down on {let a = a.clone(); || a.something()}-style manual move(clone) boilerplate would be amazing. But I don't think Handle is any help in that.

What I think we need is more capture modes to complement the existing destructive move:

  • move(copy) - move only things that are Copy, error on non-Copy captures.
  • move(clone) - also move things that are Clone, via Clone

Would this mean making Clone more magic? Perhaps.

5 Likes

Being able to specify capture mode per parameter would help a lot. For once C++ got this more right than Rust.

4 Likes