Most active commenters
  • jhugo(5)

←back to thread

128 points RGBCube | 17 comments | | HN request time: 0.815s | source | bottom
1. jhugo ◴[] No.44498376[source]
> we cannot just require all generic parameters to be Clone, as we cannot assume they are used in such a way that requires them to be cloned.

No, this is backwards. We have to require all generic parameters are Clone, as we cannot assume that any are not used in a way that requires them to be Clone.

> The reason this is the way it is is probably because Rust's type system wasn't powerful enough for this to be implemented back in the pre-1.0 days. Or it was just a simple oversight that got stabilized.

The type system can't know whether you call `T::clone()` in a method somewhere.

replies(4): >>44498401 #>>44498749 #>>44499040 #>>44499325 #
2. berkes ◴[] No.44498401[source]
> The type system can't know whether you call `T::clone()` in a method somewhere.

Why not?

replies(1): >>44498701 #
3. jhugo ◴[] No.44498701[source]
Types don't carry behavioral information about what the method does internally. Everything about a method is known from its signature.

The compiler doesn't introspect the code inside the method and add additional hidden information to its signature (and it would be difficult to reason about a compiler that did).

replies(2): >>44498750 #>>44498864 #
4. enricozb ◴[] No.44498749[source]
For structs, why couldn't rust check the necessary bounds on `T` for each field to be cloned? E.g. in

    #[derive(Clone)]
    struct Weird<T> {
      ptr: Arc<T>,
      tup: (T, usize)
    }

for `ptr`, `Arc<T>: Clone` exists with no bound on `T`. But for `tup`, `(T, usize): Clone` requires `T: Clone`.

Same thing for other derives, such as `Default`.

replies(1): >>44498755 #
5. delta_p_delta_x ◴[] No.44498750{3}[source]
> Types don't carry behavioral information about what the method does internally.

I was under the impression type inference meant that the implementation of a function directly determines the return type of a function, and therefore its signature and type.

replies(3): >>44498799 #>>44498834 #>>44498836 #
6. jhugo ◴[] No.44498755[source]
Because it doesn't know if you're relying on T being Clone in method bodies. The internal behavior of methods is not encoded in the type system.
replies(3): >>44498847 #>>44499243 #>>44508105 #
7. jhugo ◴[] No.44498834{4}[source]
While you can sometimes elide the return type (and what you describe only happens in closures — `|| { 0u32 }` is the same as `|| -> u32 { 0u32 }` — methods and free functions must always have an explicitly declared return type), that's not the same thing as being described above.

For the existence of any invocation of `<T as Clone>::clone()` in the method body to be encoded in the method signature, we'd either need some wild new syntax, or the compiler would need to be able to encode hidden information into types beyond what is visible to the programmer, which would make it very hard to reason about its behavior.

8. mryall ◴[] No.44498836{4}[source]
No, Rust functions have to declare their return types. They cannot be inferred.
9. RGBCube ◴[] No.44498847{3}[source]
What?

The way the derives work is they generate code by utilizing the fields and their types. Here is a trivial implementation (of a custom trait, rather than Clone. still holds though) which will prove you wrong:

<https://github.com/cull-os/carcass/blob/master/dup%2Fmacros%...>

10. ninkendo ◴[] No.44498864{3}[source]
> Types don't carry behavioral information about what the method does internally.

I don’t remember specifics, but I very distinctly remember changing a method in some way and Rust determining that my type is now not Send, and getting errors very far away in the codebase because of it.[0]

If I have time in a bit I’ll try and reproduce it, but I think Send conformance may be an exception to your statement, particularly around async code. (It also may have been a compiler bug.)

[0] It had something to do with carrying something across an await boundary, and if I got rid of the async call it went back to being Send again. I didn’t change the signature, it was an async method in both cases.

replies(2): >>44499134 #>>44499138 #
11. chrismorgan ◴[] No.44499040[source]
> The type system can't know whether you call `T::clone()` in a method somewhere.

It’s not about that, it’s about type system power as the article said. In former days there was no way to express the constraint; but these days you can <https://play.rust-lang.org/?gist=d1947d81a126df84f3c91fb29b5...>:

  impl<T> Clone for WrapArc<T>
  where
      Arc<T>: Clone,
  {
      …
  }
replies(1): >>44499948 #
12. kd5bjo ◴[] No.44499138{4}[source]
`Send`, `Sync`, and `Unpin` are special because they're so-called 'auto traits': The compiler automatically implements them for all compound types whose fields also implement those traits. That turns out to be a double-edged sword: The automatic implementation makes them pervasive in a way that `Clone` or `Debug` could never be, but it also means that changes which might be otherwise private can have unintended far-reaching effects.

In your case, what happens is that async code conceptually generates an `enum` with one variant per await point which contains the locals held across that point, and it's this enum that actually gets returned from the async method/block and implements the `Future` trait.

13. saghm ◴[] No.44499243{3}[source]
You can already write method bodies today that have constraints that aren't enforced by the type definition though; it's trivially possible to write a method that requires Debug on a parameter without the type itself implementing Debug[0], for example. It's often even encouraged to define the constraints on impl blocks rather than the type definition. The standard library itself goes out of its way to define types in a way that allow only partial usage due to some of their methods having bounds that aren't enforced on the type definition. Rust's hashmap definition in the standard library somewhat notably doesn't actually enforce that the type of the key is possible to hash, which allows a hashmap of arbitrary types to be created but not inserted into unless the value actually implements Hash[1].

[0]: https://play.rust-lang.org/?version=stable&mode=debug&editio...

[1]: https://www.reddit.com/r/rust/comments/101wzdq/why_rust_has_...

14. almostdeadguy ◴[] No.44499325[source]
All the #[derive(Clone)] does is generate a trait impl of Clone for the struct, which itself can be bounded by trait constraints. It doesn't have to know that every use of the struct ensures generic parameters have to/don't have to be Clone. It doesn't have to make guarantees about how the struct is used at all.

It only needs to provide constraints that must hold for it to call clone() on each field of the struct (i.e. the constraints that must hold for the generated implementation of the fn clone(&self) method to be valid, which might not hold for all T, in which case a Struct<T> will not implement Clone). The issue this post discusses exists because there are structs like Arc<T> that are cloneable despite T not being Clone itself [1]. In a case like that it may not be desirable to put a T: Clone constraint on the trait impl, because that unnecessarily limits T where Struct<T>: Clone.

[1]: https://doc.rust-lang.org/std/sync/struct.Arc.html#impl-Clon...

15. jhugo ◴[] No.44499948[source]
Yeah. My bad. I got annoyed by the "is broken" terminology of TFA and wasn't thinking clearly :/
replies(1): >>44499988 #
16. chrismorgan ◴[] No.44499988{3}[source]
I did the same… I just deleted my comment quickly when I realised I had erred!
17. enricozb ◴[] No.44508105{3}[source]
I'm not sure how these two things are related. When writing an `impl` for a struct, there's no assumption on the bounds of the generics (if any) unless they are specified _at the impl site_.

For example, the bounds of T in

    impl<T> Weird<T> {
      ..
    }
are independent of the bounds of `T` in any other impl or the struct definition.

Unless I'm missing something...