Pre-RFC: `#[derive(From)]` for newtypes

TLDR: this is a mini-proposal/pre-RFC about enabling #[derive(From)] for newtype wrappers (tuple structs with a single field).

Note that this is not fully structured as an RFC yet, I just wanted to get a vibe check first.

Motivation

I think that the primary motivation should just be to "fill in the missing blanks" where the solution is obvious, but I'll show a concrete use-case.

A common idiom to improve the type safety of Rust code is to use newtype wrappers on top of another type. For example, we can wrap a number to represent things like Priority(i32), PullRequestNumber(u32) or TCPPort(u16).

When implementing such a wrapper, it is useful to expose certain operations on the newtype that delegate to the inner type. Things like PartialEq, Debug, Copy/Clone, etc. That is usually done using the built-in derive macro:

#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Copy, Clone)]
struct TcpPort(u16);

Which is quite terse and doesn't require any macros or third-party crates.

However, not all standard library traits can be derived in this way. Notably, it is not possible to derive an implementation of the From trait. In some cases, where the newtype should validate its contents (e.g. struct Email(String)), From is not desirable, but in many cases the newtype can accept any value of the inner type, and thus implementing From for it is quite natural.

Writing the implementation by hand is easy, and takes just ~4-5 lines of code. However, when you want to use many newtype wrappers in your codebase, this can get annoying quickly. In that situation I usually either:

  • Just use a type alias instead and tell myself that I will convert it to a newtype "later" (which often doesn't ever happen).
  • Create a small declarative macro that generates the boilerplate for me. But creating a macro obfuscates the code and requires me to e.g. determine some a way to tell the macro which specific traits should be implemented (for example for some newtypes I don't want to implement Eq).

Proposal

From cannot be currently derived because it is unclear how it should behave in all situations where deriving is possible (for enums, struct with named fields, struct with multiple fields etc.). I propose to enable #[derive(From)] in a single place - when used on a tuple struct with a single field. In this case, it is 100% clear what should the compiler do, and there is (in my opinion) no question about how should such a derive be expanded.

So this:

#[derive(From)]
struct TcpPort(u16);

would generate the following code:

struct TcpPort(u16);

impl From<u16> for TcpPort {
    fn from(value: u16) -> Self {
        Self(value)
    }
}

For all other places (tuple structs with zero or multiple fields, structs with named fields, enums, etc.), #[derive(From)] would still be disabled.

This is forward-compatible, as we can expand the set of places where #[derive(From)] will be allowed in the future.

For example, we could allow using it for structs with multiple fields, where a single field would be annotated with #[from] and the rest would implement Default. But that's something that is much more bikeshed-worthy, so I explicitly leave out of this proposal.

It shouldn't run into any orphan rule problems, because at the place of expansion, we necessarily own the type for which we implement From.

As a part of this change, we could maybe also enable it for structs with a single named field, but that seems slightly less obvious to me, and it is also not so common for the newtype pattern.

A related functionality could be deriving Into (or rather From<Newtype> for <InnerType> to convert back into the inner type), but again, that's more bikeshed-worthy, so I wouldn't include it in this RFC.

Prior art

  • The popular derive_more crate enables deriving From for single-field tuple structs. It even mentions the same motivation:

    Rust has lots of builtin traits that are implemented for its basic types, such as Add, Not, From or Display. However, when wrapping these types inside your own structs or enums you lose the implementations of these traits and are required to recreate them. This is especially annoying when your own structures are very simple, such as when using the commonly advised newtype pattern (e.g. MyInt(i32)). This library tries to remove these annoyances and the corresponding boilerplate code. It does this by allowing you to derive lots of commonly used traits for both structs and enums.

    Apart from the single-field tuple struct variant, it also supports other use-cases, e.g. deriving From on enums.

    • Crates that currently use derive_more only for the simplest (but likely most common) use-case with single-field tuple structs could instead just use the built-in derive.
  • derive-from-one offers similar, although less comprehensive, functionality.

  • This idea was floated around in New derives: From and Deref · Issue #2026 · rust-lang/rfcs · GitHub, but it was combined with deriving the Deref trait, and I think that it was larger in its scope.

Was there any prior discussion about this? I haven't found anything apart from the one issue above. I would be glad for any feedback.

10 Likes

I think the only question I would have is whether it's sufficiently clear to people that derive(From) creates an impl From<Inner> for Wrapper, rather than an impl From<Wrapper> for Inner (which you suggested using derive(Into) for.

I do personally think that's sufficiently clear, but I think it'd need some clear documentation, at least.

I'd also love to see Deref, AsRef, and AsMut. (All of those should be independent, as you might want some of them but not others.)

5 Likes

Since all derives on type T generate code in the form of impl Trait for T, I think that this wouldn't be especially confusing, but documentation would help, of course.

Btw, I thought of one case where this might not be forward compatible. If we decided in the future that deriving From on a tuple struct with N fields should generate From<Tuple of size N>, then on a single field struct it would actually generate From<(Inner,)> rather than From<Inner>. But I don't think that generating the From impl from tuples is a great idea, and it definitely doesn't make sense for the common case when N=1.

4 Likes

I would like to see AsRef<Self> and AsMut<Self> derives as well, for any type at all.

If I saw...

#[derive(AsRef)]
struct Something ...

...my first assumption would be AsRef<Self> (even if on a single tuple type).

2 Likes

Specifically for newtypes, derive(From) only really has the one possible implementation, so it makes a lot of sense to provide. You can already write Wrap(val), the derive just provides the generic version of the same operation.

1 Like

I see at least two ways to expand it impl From<Inner> for Wrapper and impl<T> From<T> for Wrapper where Inner: From<T>

Sure, that is a possibility I suppose, but not the most straightforward one. None of the existing derive macros for built-in traits work like this (and I don't think they should), so IMO users wouldn't expect From to behave like this.

Any reason why you're restricting it to tuple structs? I like using structs with named fields, and it'd be nice if I could opt into something like:

#[derive(From)]
struct TcpPort {
    port_number: u16
}

Not that the name is required here, as the u16 being the port number is obvious to anyone even vaguely familiar with TCP as a protocol, but sometimes I'd like to write something similar for protocols where people aren't so universally familiar with it (or this would be useful if someone was writing code to be read by people not familiar with the innards of TCP).

3 Likes

Of note here: making it impl<T> From<T> for Wrapper where Inner: From<T> would conflict with an impl From<Wrapper> for Inner (duplicates the blanket From<T> for T impl on Wrapper), which is sometimes useful (it lets you do tcp_port.into() for APIs expecting a u16, in OP's example), so I don't think we should expand the standard From derive this way.

I had this remark in the proposal:

As a part of this change, we could maybe also enable it for structs with a single named field, but that seems slightly less obvious to me, and it is also not so common for the newtype pattern.

I think that permitting it for single-field named structs would be fine, but it would also expand the scope of the RFC a little bit. But both works.

1 Like

I believe that derive_more is already a de-facto standard, with its Display, Constructor, math derives and more. I mean rust already has bucnh of batteries in 3rd party crates (like chrono, or rand, or syn/quote, ...), I think I am happy with status-quo.

1 Like

It probably would be an unpopular opinion, but I wish we could eventually get newtype as a language feature.

Just imagine the beautiful future where your type aliases are actually distinct types.

newtype Length = u8;

let mut a: Length = 5;
let b: Length = 4;
let mut c: u8 = 3;
let d: u8 = 2;

a = a - b;

// compile error, Length is a distinct type from u8
a = b + c
1 Like

I would love to see something equivalent to this, using some kind of delegation. Suppose you could write struct Length(u8) together with some short indicators of what things you want to delegate to u8 and what things you want to disallow. Would that work?

1 Like

a - b is actually a really good example of why this is tricky. If you subtract a length from a length you do get another length; however, if you subtract an Instant from an Instant you get a Duration, preventing you from implementing Instant as (e.g.) a newtype of Duration. So you end up having to spell out your delegation strategies after all, until what you have is basically just derives again. Smart, useful derives that it would be good to have a pattern for! Maybe even dedicated syntax! But that’s where the syntax belongs: not replacing struct or type, but derive.

4 Likes

I would simply love an introduction of newtypes to the language.

Maybe it is worth preparing a RFC to see what people think about it?

I think this should work on anything that #[repr(transparent)] works for, which is any single field struct.

Isn't struct Length(u8) already the de-facto standard for newtype, just that it can require a lot more typing in the derive? Imagine we had a way to "bundle" multiple derives together (I'm not speaking of a derive macro here):

// Or some other syntax, this one potentially even as part of the prelude
// if newtypes are common enough
type NewType = Debug + From + ...;
type Math = Add + Sub + Mul + ...;

// Ideally allow anyone to specify Derive (and trait bound?) collections/bundles,
// Would likely need a different syntax.
type Common = Debug + serde::Serialize + serde::Deserialize;

#[derive(NewType, Math)]
struct Length(u8);

// No Math because it has a different return type
#[derive(NewType)]
struct Instant(u64);

#[derive(Common)]
struct Length{...};

Yes, it's still more than newtype Length = u8, but it's also more flexible and allows specifying what exactly you need.

As for the above mentioned trait bounds: Imagine serde having this:

// In serde (may need to be split between Trait bundle/alias and
// Derive bundle/alias)
type Serde = Serialize + Deserialize;

// In crates using serde
#[derive(Serde)]
struct MyStruct{...}

fn do_something(T: Serde) {...}

Or tokio this:

type Task = Send + Sync + 'static;

// Simplified
pub fn spawn<T: Task>(task: T) {...}

Unless I'm mistaken, this currently requires a third trait with auto-impls if both Serialize and Deserialize are present and would require another proc macro for derive.

3 Likes