QuartzLibrary

Splitting the Rust Copy Trait

TL;DR:

Rust has the opportunity to significantly improve its ergonomics by targeting one of its core usability issues: passing values across boundaries.

Specifically, the ability to opt into 'copy semantics' for non-Copy user types would solve a host of issues without interfering with lower-level code and letting users opt into ergonomics ~on par with garbage collected languages.


Copy & Move semantics

As a reminder, in Rust when a value is assigned, moved into a closure 1 , or passed to a function:

This post will not try to establish more context than this, but if you have used Rust for a while this will all sound familiar. See Copy and Clone for more info.

As a full time Rust developer writing code across the stack2, I enjoy the control and guarantees the language offers. But. There's set of related issues that wears down on Rust's utility in a lot of code:

Example
struct Post {
  author: Username,
}
fn main() {
  let post = Post { username: todo!() };
  lookup(post.username);
  //     ^ Compile error.
  // This often gets in the way!
}
fn lookup(value: &Username) -> User { ... }

The above are small, but frequent, pain-points for code that simply does not need to care about, say, an Rc::clone. This is a genuine productivity sink. I have forgotten a meaningless .clone() many times, leading to slow downs in my iteration speed as the compiler churns. Even when iteration speed is not the bottleneck, extra annotations can often go from useful guardrails to obscuring fog over the logic they are sprinkled on.

Countless times, small paper-cuts shave off slivers of productivity even for experienced Rust devs when the language should just get out of the way.

Yet, those clones do count in other contexts. We also have mental models of them—for example, they help maintain useful scoping invariants3—on top of the usual performance considerations. Rust can't just turn all of that onto its head, it's part of what makes it great!

A solution of hopefully small compromises

Desired properties:

Proposed solution:

The trait would look something like:

/// Invariants:
/// - The clone has ~no side effects, like for an Rc.
/// - The clone may be skipped.
/// NOTE: unsafe code should not rely on a clone (not) happening.
trait CopySemantics: Clone {}

// A blanket impl is the simplest option, but we could also
// use editions to improve the semantics of the trait by
// disentangling it from `Copy` types.
impl<T: Copy> CopySemantics for T {}

The pros of this solution:

The cons of this solution:

Alternatives:

Assorted closing notes

Conclusions

Rust could become significantly more ergonomic in high-level code by allowing users to opt into 'copy semantics' for their types, which would then behave like current Copy types. By allowing this, but not using it in the standard library and core crates, we avoid permeating the ecosystem with the pattern, while letting higher level crates reap the benefits where they are most plentiful.


  1. and async blocks, but those are similar, so I'll just talk about closures.

  2. Including in what might be one of the most complex Rust browser frontends in the world, though mostly by virtue of not there being many of them. :D

  3. For example 'I have this Rc here, but I haven't cloned it before passing it into the closure, and I am using it below, and it compiles, so it is not used in the closure'. Lots of small invariants like this add up to code that is simpler to grok.

  4. As in: a beginner would not have an extra thing to learn, which I think is true for the proposed solution. A beginner would still have to learn about 'copy' and 'move' semantics, but wouldn't need to care about the, now deprecated, connection to the Copy trait. They can separately learn about the Copy trait if they need or want to.

  5. Unless the destination has opted into more restrictive checks.

  6. move() can mean 'no captures'.
    move(..) could mean move (move everything).
    move(a) could mean 'capture by move exactly a'
    move(ref a) could mean 'capture by reference a'
    move(a, ..) could mean 'capture by move at least a'
    move(a, ref ..) could mean 'capture a by move, capture by reference everything else'.
    Though not all of these need be implemented.