Why is there no TryClone trait?

As far as I can tell, there are currently 10 implementations of try_clone in the standard lib, with exactly the same signature (pub fn try_clone(&self) -> io::Result<Self>):

Other popular libraries like tokio also include some structs with try_clone methods (see here for example).

I was a little disappointed when I realised that there is no standard trait for this method. I know I could just create my own marker trait, but considering that the amount of try_clone-method implementations are in the double digits in the standard library alone, I was asking myself if there is would be a downside to adding a TryClone trait to the standard library directly:


trait TryClone {

fn try_clone(&self) -> std::io::Result<Self>;

}

or even with a generic error (similar to TryFrom):


trait TryClone {

type Error;

fn try_clone(&self) -> Result<Self, Self::Error>;

}
3 Likes

Could you give a little context as to why you are abstracting over TryClone specifically? These types are a little diverse, and so far just seem to be following convention.

3 Likes

It might be better to just use TryFrom<&T> for this kind of thing.

3 Likes

It could be useful. Fallible collections has TryClone for handling out of memory errors:

5 Likes

IMO, in an ideal world, Clone would be an alias for From<&Self>. See also recent Zulip discussion

4 Likes

So I have library with a struct which wraps any struct that implements Read / AsyncRead (obviously it does some stuff with the reader, but that is not really relevant for this).

I can make this struct cloneable, if the reader is cloneable:

struct MyStruct<R> {
    reader: R,
}

impl<R: Clone> Clone for MyStruct<R> {
    fn clone(&self) -> Self {
        Self {
            reader: self.reader.clone(),
        }
    }
}

Now it would be nice to do the same thing for try_clone:

impl<R: TryClone> MyStruct<R> {
// impl<R: TryClone> TryClone for MyStruct<R> { // this would obviously be even better
    fn try_clone(&self) -> std::io::Result<Self> {
        Ok(Self {
            reader: self.reader.try_clone()?,
        })
    }
}

but instead I either have to implement MyStruct::try_clone separately for all types I want to support (i. e. one impl for MyStruct<std::io::File>, one impl for MyStruct<tokio::io::File> and so on) or I have to implement my own trait for all those structs and can then implement try_clone generically like in the code above. Both solutions require a explicit implementation of either me or the author of the library which includes the Read / AsyncRead struct. (The latter is unlikely for a small library like mine :D)

Concerning TryFrom<&T>:
I could also implement a try_clone for MyStruct generically with a generic constraints TryFrom<&Self>, but for example File has a try_clone but no TryFrom<&Self> implementation.


Long story short: I know how I can solve my issue. I'm just a bit unsatisfied that there is no standard TryClone trait. It seems like a logical addition to the language.

7 Likes

If Rust supported associated type defaults, the Clone trait could look like this:

trait Clone {
    type Error = !; // or Infallible

    fn clone(&self) -> Self;

    fn try_clone(&self) -> Result<Self, Self::Error> {
        Ok(self.clone())
    }
}
2 Likes

This doesn’t actually work? It’s impossible to implement clone method if the error isn’t !

1 Like

I like what @kornel is proposing by extending the existing Clone trait. But I think there are some downsides to that exact approach which I think are worth expanding on. I will finish by formulating an alternative approach using a potential combination of experimental language features.

Blanket Extensions and Effect Mismatches

A risk of just adding a Clone::try_clone method is that it then becomes available for every type currently implementing Clone. Implementations like RefCell::clone may panic - which means that when people call RefCell::try_clone, the method may not return an error like they expected.

// Was this method intentionally implemented, or is it still the default impl?
// If something goes wrong, will this method return an error, or will this
// panic on us? It's impossible to tell from just the signature alone.
my_type.try_clone();

To make it work, the implementation would need to be updated first - but from the signature alone users have no way to know which impls have been updated. Compare that to a dedicated TryClone trait which does not have default or blanket impls. If it's implemented we know for sure (modulo programmer errors) that if a try_clone method is present, it should return an error if something didn't go right.

Effect Generics

Effect-generics may provide a way to provide the benefits of both approaches: both to get the type safety of dedicated traits, but without the duplicate trait definitions. Just for this example let's use an attribute-based syntax as a placeholder, together with the placeholder try..yeets keywords. We can imagine it working something like this:

// An updated `Clone` trait which may optionally be fallible.
// If we implement `Clone for T` it's not fallible. If we implement
// `try Clone for T` it *is* fallible. This should be backwards-compatible
// with the existing `Clone` trait.
#[maybe(try)]
trait Clone {
    #[maybe(try)]
    fn clone(&self) -> Self;
}

// This is our infallible implementation for the type `MyCat`.
// Because we didn't specify `impl try Clone`, the `fn clone`
// also is not able to return any types implementing `Try`.
impl Clone for MyCat {
    fn clone(&self) -> Self { .. }
}

// This is our fallible implementation for `OtherCat`. Here we
// specified `impl try Clone`, which signals that the method should
// be `try fn clone`. The way fallible functions are expected to work
// is that they will carry some way to specify the type signature,
// which in this case we're hard-coding to: "this function may return
// a `Result<Self, io::Error>`.
impl try Clone for OtherCat {
    try fn clone(&self) -> Self yeets Err(io::Error) { .. }
}

Neither the syntax for effect generic impls or fallible functions has been locked in, so the syntax feels a bit more clunky than whatever final version we might end up with. But I hope the semantics of the design come through here: there would remain exactly one trait definition, which can be implemented as either fallible or infallible.

@matklad The ! type is a default, not a bound, so I assume it'd be possible to override with another type.

However, this could be less ambitious, and hardcode some std::clone::Error. Maybe the clone error could be a wrapper around Option<Box<dyn Error>>?

@yoshuawuyts I assume that before try_clone lands, std would update its Clone implementations to use try_clone "natively" wherever possible instead of panicking or aborting, and the ecosystem would eventually follow. The default fallback can also be something like this:

fn try_clone(&self) -> Result<Self, std::clone::Error> {
    catch_unwind(|| self.clone()).map_err(std::clone::Error::new)
}
impl Clone for File {
    type Error = io::Error;

    fn clone(&self) -> Self {
        // there’s nothing reasonable I can write here
    }

    fn try_clone(&self) -> Result<Self, Self::Error> {
        // call dup here, return error
    }
}

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