Hey y'all,
Totally academic question, but let's have some fun. What kind of things can you come up with to do with an unconstrained type parameter?
I'll start with the identity function
fn identi<T>(x: T) -> T {
x
}
Hey y'all,
Totally academic question, but let's have some fun. What kind of things can you come up with to do with an unconstrained type parameter?
I'll start with the identity function
fn identi<T>(x: T) -> T {
x
}
I believe there is a theorem about unconstrained types only admitting the identity function. (And technical variations thereof – you can also put it in a wrapper, e.g. vec![x]
). I may be wrong about this, as I don't recall where and in what context I read this claim.
You can also call std::any::type_name_of_val(&x)
, std::mem::size_of_val(&x)
and similar very generic functions.
You could also use the slice-Copy
specialization trick to learn informations about the traits it (may, see why specialization is unsound) implement (though without being able to call them). (Edit: WARNING: this isn't something you should generally do since it has caveats and is not even guaranteed to work)
I don't know this yet. Can you show an example?
You can pretend that your function can return something else eventually, it's just taking a very-very-very long time. In Haskell any function can diverge and the value in this case is called bottom which you write as _|_
.
fn magic<T>(x: T) -> u32 {
loop {}
}
T
is constrained a little in your example. Function parameters and return values have to be Sized
so you couldn't weaken the constraint to T: ?Sized
.
With Sized
types can do things like permute tuple elements, e.g. map (x, y)
to (y, x)
.
Well I guess you could as long as you get it behind an indirection.
Here you go Rust Playground
Edit: WARNING: this isn't something you should generally do since it has caveats and is not even guaranteed to work
You mean like this:
fn rot<T>((a, b, c, d, e, f): (T, T, T, T, T, T)) -> (T, T, T, T, T, T) {
(b, c, d, e, f, a)
}
Ha! Would never have imagined it was possible to write such a thing.
Again, please add disclaimers to this. Stuff like it may lead to UB and it isn't part of any guarantee anywhere.
It's more "things you may happen to be able to observe on a current rustc" not "things you can [per spec] do with these types".
I don't like that this is repeatedly brought up as if it were a thing that users could use rather than an implementation wart.
I mean… why wouldn't it be possible? I can't really get what is surprising about swapping the elements of a tuple…
I should have said:
"I would never have imagined it was possible...". It's more a statement about my limited imagination and understanding of Rust than anything else.
It was not the swapping of the elements of a tuple that surprised me. The thing that raised my eye brow there is being able to specify as a parameter as '(a, b): (T, T). I'm not used to seeing anything other than naked identifiers to the left of the colon
t: (T, T)`. Not that I have ever seen a tuple used as a parameter spec that way either.
I'm curious, where is the UB in any suggestion in this thread so far? I have not seen any unsafe
yet.
I guess Rust could change to break them. But where is this "spec" of which you speak?
Okay, that's going straight into my Rust Curiosities Iceberg Chart, in the Abyss section.
I'm curious, where is the UB in any suggestion in this thread so far? I have not seen any
unsafe
yet.
I'm saying that attempts to use this pattern may eventually lead to UB. So don't. It's not blessed and it's definitely a hole in the type system (and internally documented as such). If you try to use it in production code and have arbitrary types flow into it then who knows what types users would pass into it. Eventually you might end up with a == a
being false.
And then we'd be faced with the unpleasant situation of either removing it or telling people that their safe code is unsound because someone else integrated this pattern into their crate because the example wasn't properly labeled as dangerous.
I don't mind it being mentioned as curiosity. I do mind any pretense of this being usable or safe.
I guess Rust could change to break them. But where is this "spec" of which you speak?
In this case the API contract of the Copy
and Clone
traits, where you'll notice the absence of any such guarantee.
Any irrefutable pattern (a pattern that always matches) may appear in the place of an identifier in let bindings, function parameter declarations, and these days even as the left-hand side of the assignment operator[1]. Indeed, from Rust's viewpoint the identifier in something like foo: Foo
is just a catch-all pattern like you might use in at the end of a match
expression.
Refutable patterns can also appear in a variable binding if it's an if let
, while let
, or the recent-ishly stabilished let..else
construct.
Swapping two values with (foo, bar) = (bar, foo)
is now valid Rust! ↩︎
This is an interesting point because the problem with Foo
in the playground is so subtle. It may lead to UB, but the only thing that can lead to UB (in the strictest sense) is unsafe
code that relies on an invariant which is broken. And indeed, there is no unsafe
present in the example's surface syntax.
The invariant in this case, as I understand it, is whether a type has a trivial constructor and destructor (Copy + Clone
) or either of these are non-trivial (Clone + !Copy
, if I can overload trait bounds syntax for illustration). The Foo
type fibs about the complexity of its constructor, and that allows it to observe unusual effects. Worse still is that Foo
is not usable at all with unsafe code that relies on the invariant that Copy
types have a trivial constructor and destructor, because that is UB.
I don't know if it has ever been proposed (it would surprise me if it hasn't) and even though it's too late to change now, Copy
probably should have been marked unsafe
. This idea really sat with me after watching one of Niko's talks where he describes Copy
as "Safe to memcpy". I don't know if he meant this literally, but I have to suspect it is so considering this slide juxtaposes it against Send
!
It's documented that "any type implementing Drop
can’t be Copy
" and the compiler won't allow it. But I don't know of anything that forbids non-trivial constructors from implementing Copy
, not even through documentation or lints.
There may be some path though impl Trait for Baz<'static>
where that then gets fed into the Copy
which turns it into a impl Copy for Baz<'static>
and the answer
gets proven into some heisenstate that's true and false at the same time. Or perhaps someone manages to chain it together with some 3rd-party unsafe things.
But I don't really care. Why do we even have to argue about relying on the a) implementation detail b) with scary warnings c) with unsafe compiler-internal marker traits?
Why do people try so hard to be part of the problem?
Yeah. You can also move stuff around in an array, even in-place (using split_at_mut
): any permutation can be decomposed into swaps.
One question. Why did you call the function implements_eq_modulo_lifetimes()
and not implements_copy_modulo_lifetimes()
?
Other than that very interesting detail. But that is definitely weird. From the documentation of Clone
:
Types that are
Copy
should have a trivial implementation ofClone
. More formally: ifT: Copy
,x: T
, andy: &T
, thenlet x = y.clone();
is equivalent tolet x = *y;
. Manual implementations should be careful to uphold this invariant; however, unsafe code must not rely on it to ensure memory safety.
I find this language to be a little too soft, considering that array.clone()
will call copy on its members instead of clone. Of course I can't really think of a case where it would be good idea to have a Copy
type implement Clone
in a different way. But still... Maybe the documentation could mention that the standard lib for T: Copy
may rely on copy for implementations of clone on it's own container types.
Of course I do understand that they didn't want to make Clone
unsafe, forcing the user to uphold the invariant.