rfcs
rfcs copied to clipboard
New derives: From and Deref
It's often useful to wrap a single type into a new one, to be able to add new methods, provide more documentation and possibly type safety. For example,
pub struct Inches(f32);
pub struct Meters(f32);
This way if the units are used incorrectly we get a type error. Fortunately there is no space overhead as well. This is very similar to newtype in Haskell.
I'm proposing that we consider being able to derive From and possibly Deref for structs with exactly one field. For the above example, adding #[derive(From, Deref)] above Inches would generate the code:
impl From<f32> for Inches {
fn from(x: f32) -> Self {
Inches(x)
}
}
impl Deref for Inches {
type Target = f32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
These or similar derives would significantly reduce boilerplate when creating wrapper types, thus encouraging their use.
EDIT: fixed i32 => f32
Possibly this could be done as a macro, though.
Not sure if it's any good, but there is a crate that does just that: https://github.com/JelteF/derive_more
Disclaimer: I might come across a little rude because english isn't my first language and text isn't very good at conveing emotion in the first place. I'm not writing with animosity towards you - literally the contrary!
I think I'm against this, for the following reason: Syntax and implications.
#[derive(From, Deref)] could confuse many people. Deriving from a very generc trait and making it only legal for rather specific types is odd. I mean what would happen if one attempts to derive these traits for invalid types? "Sorry, your type is not tall enough to ~~ride this ride~~ derive from this trait". Is this really a good idea? Do we really want these specific auto-impls in derive? If I derive from something, I expect to always get a predictable implementation, without errors (excluding other trait requirements, like Eq => PartialEq).
Also I think automatically implementing From for newtypes is not a good idea either. Isn't the point of newtypes to ensure that the data they wrap cant be passed / used somewhere wrong? In other words, binding meaningless data types to specific, statically checked, meaning? Let me reuse your Inches / Meters example:
Let's imagine we have derived From and Deref for both Inches and Meters (or implemented it manually, doesn't really matter). There's nothing stopping me or anyone else from doing stuff like this:
let inches = Inches(123.0);
// Somewhere, in the dark corners of your codebase:
let meters = Meters::from(*inches);
// Rather than:
let meters: Meters = inches.into();
// Or
let meters = inches.into_meters();
The from-derive variant avoids explicit conversion of the data and would probably introduce bugs to our code.
I believe it's vital to design your API or code generally in such a way, that it cannot be used incorrectly (Scott Meyers, anyone?). From and Deref would make it very easy to use it incorrectly.
Wow, I really don't know how to keep it short. What do you guys think?
Deriving from a very generc trait and making it only legal for rather specific types is odd.
As far as I know, #[derive(Copy)]/Clone/Default/Eq/Ord/etc are only legal on types that contain only other Copy/Clone/Default/Eq/Ord/etc types. So this is nothing new.
The from-derive variant avoids explicit conversion of the data and would probably introduce bugs to our code.
If you have a newtype/wrapper for which the obvious, trivial conversion is invalid, wouldn't you simply not use this feature and write a correct From impl maually? It's not that hard to write a broken From impl today.
automatically implementing From for newtypes
I believe the proposed feature is meant to be opt-in, like existing derive annotations. Most of your post simply doesn't make sense to me; it sounds as if you're talking about an opt-out feature that users would have to know to avoid, rather than an annotation that doesn't do anything until you explicitly add it.
As far as I know, #[derive(Copy)]/Clone/Default/Eq/Ord/etc are only legal on types that contain only other Copy/Clone/Default/Eq/Ord/etc types. So this is nothing new.
True, but what I was talking about was #[derive(From / Deref)] mandating a syntactically specially formed struct (a tuple struct with only one field). What would a programmer, who's unfarmiliar with the derive, think when they came across #[derive(From)]? Would it be clear what it does?
I'd prefer if we would choose more distinct names for the derives, something like #[derive(NewtypeFrom)], but thats not possible I think. How about a custom attribute #[WrapperType], which would implement both Deref and From? That would be totally fine with me.
If you have a newtype/wrapper for which the obvious, trivial conversion is invalid, wouldn't you simply not use this feature and write a correct From impl maually? [...]
Yup, that's what I was trying to show with my snippet, although I was doing it the other way around with Into.
Comments?
Deref seems quite reasonable, and conveniently there's already a feature that defined "the one field that matters, even if there are extra ZST markers": repr(transparent). (That probably doesn't work if every field is a ZST, but Deref to a ZST feels like an uncommon enough scenario that it doesn't worry me.)
From I'm more torn about. If one can derive From, it feels like one should also be able to derive Into, but that's something we tell people not to implement (if possible). So what I really want for a newtype that has an invariant is InnerFromSelf, but that feels meh.
I remember seeing "we should have explicit support for declaring a newtype" expressed before, but can't seem to fine it...
Deriving From automatically derives Into. If I recall correctly, you get an error if you attempt to derive Into yourself. I don't see the problem there.
@scottmcm I think deriving From is enough; Into will be auto-impled (as @iitalics pointed out). But I do like the idea of InnerFromSelf except maybe it makes more sense as IntoInner instead.
There is a derive_deref crate with ~68000 downloads for now, and I believe this is a good evidence that #[derive(Deref)] is in demand.
Personally I've been implementing a trivial Deref each time for a newtype, where I use newtype construction for a flow control between interfaces rather than for hiding an internal representation.
The fact that a crate can already reliably do this and also the fact that this isn't an essential part of letting the ecosystem pick a "standard" datatype for something means that this seems like a low priority.
Not that I'm against the idea, but if there's already a crate for it, what does putting it into core gain us?
@Lokathor That same argument can be made for all of the built-in derives in Rust.
It would be interesting to survey the uses of derive_deref. In particular, how many of its uses are "real" Derefs as opposed to using Deref as a workaround for the lack of delegation.
It's also not clear to me that "If the type of that field is a reference, the reference will be returned directly." is the correct default behavior for Deref impls in general.
Unfortunately I don't have the time myself to investigate either point.
Yes, I'd worry that #[derive(Deref)] encourages deref polymorphism, which bring subtle problems. At least the variance issues should not be problematic here, right? I suppose Deref/DerefMut have some history with soundness bugs too, but again not with such simple derives probably.
I think rust should first sort out both (a) delegation in general and (b) convenient newtype delegation across many traits. I think (b) could reasonable precede (a) if done properly outside std/core, like maybe the newtype_derive crate, but convenient precise arithmetic, Fn, etc. delegation should be preferred over Deref/DerefMut.
Also, there is a units crate if you want units, so maybe even arithmetic traits should always omit some functionality for newtypes.
@burdges
Yes, I'd worry that #[derive(Deref)] encourages deref polymorphism
In my opinion, I'd worry that the existence of Deref trait itself encourages deref polymorphism, not the existence of #[derive(Deref)] implementation ;)
Kind of, it doesn't stop anyone to implement a little boilerplate impl Deref for MyType, and it doesn't stop using external crates implementing Deref.
I think rust should first sort out both (a) delegation in general and (b) convenient newtype delegation across many traits. I think (b) could reasonable precede (a)...
Yes, agree, if I understand you correctly. I think that seems like newtype could be a main motivation to have a quick #[derive(Deref)] over it. By the way, derive_deref works with limitations that the struct should have only a 1 field (newtype).
Not that I'm against the idea, but if there's already a crate for it, what does putting it into
coregain us?
Because Rust is promoting Zero-cost abstraction and an auto Deref impl (aka derive) is just a continuation of this promotion?
I would generally say that a newtype should implement AsRef<Inner> instead of Deref<Target=Inner> so that going to the wrapped type is an explicit step. Otherwise, why are you wrapping the inner value at all.
As to From, that seems reasonable at first, but if there's no guards or checks on the inner value at all, why is the inner field private? If it's a public field you don't really need the From impl.
So it's not that more derives couldn't be in core, but we would probably want stronger motivation for additional core derives. Since derives can be provided via a crate, the bar for adding them to core is relatively high.
As to From, that seems reasonable at first, but if there's no guards or checks on the inner value at all, why is the inner field private? If it's a public field you don't really need the From impl.
Yeps, From is open for discussion while I still see some reason to give some facilities for wrapper types. It might have complex situations, especially the fact that From can be implemented for many "variants".
I would generally say that a newtype should implement
AsRef<Inner>instead ofDeref<Target=Inner>so that going to the wrapped type is an explicit step. Otherwise, why are you wrapping the inner value at all.
"Deref coercion" is (for me) part for zero-cost abstraction and motivation for a Deref derive. As it can have only one implementation of Deref trait for a struct, it sounds "easier". Especially for singleton tuple. I suppose it must not differ too much than Default derive (in its logic).
The common meaning for "zero cost abstraction" is that it's a "zero runtime overhead abstraction". Writing &x and activating Deref or x.as_ref() and activating AsRef would both satisfy this definition. It's a matter of how explicit we have people be.
If you're bothering to use newtypes to enforce better typing on your expressions, you probably want to have more explicit changes between those types.