rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

New derives: From and Deref

Open iitalics opened this issue 8 years ago • 18 comments

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

iitalics avatar Jun 10 '17 18:06 iitalics

Possibly this could be done as a macro, though.

mark-i-m avatar Jun 11 '17 00:06 mark-i-m

Not sure if it's any good, but there is a crate that does just that: https://github.com/JelteF/derive_more

killercup avatar Jun 12 '17 07:06 killercup

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?

haudan avatar Jun 13 '17 08:06 haudan

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.

Ixrec avatar Jun 13 '17 18:06 Ixrec

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?

haudan avatar Jun 14 '17 08:06 haudan

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

scottmcm avatar Jun 29 '17 07:06 scottmcm

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.

iitalics avatar Jun 29 '17 13:06 iitalics

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

mark-i-m avatar Jul 01 '17 00:07 mark-i-m

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.

fadeevab avatar Mar 09 '20 06:03 fadeevab

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 avatar Mar 09 '20 06:03 Lokathor

@Lokathor That same argument can be made for all of the built-in derives in Rust.

Pauan avatar Mar 09 '20 07:03 Pauan

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.

Ixrec avatar Mar 09 '20 10:03 Ixrec

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 avatar Mar 09 '20 10:03 burdges

@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).

fadeevab avatar Mar 09 '20 13:03 fadeevab

Not that I'm against the idea, but if there's already a crate for it, what does putting it into core gain us?

Because Rust is promoting Zero-cost abstraction and an auto Deref impl (aka derive) is just a continuation of this promotion?

loganmzz avatar Mar 08 '25 16:03 loganmzz

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.

Lokathor avatar Mar 08 '25 16:03 Lokathor

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

"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).

loganmzz avatar Mar 08 '25 17:03 loganmzz

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.

Lokathor avatar Mar 08 '25 17:03 Lokathor