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:
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.
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:
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.
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: