Skip to content

feat: initial proposal for first class newtypes#3951

Open
papa-zakari wants to merge 1 commit intorust-lang:masterfrom
papa-zakari:first-class-newtypes
Open

feat: initial proposal for first class newtypes#3951
papa-zakari wants to merge 1 commit intorust-lang:masterfrom
papa-zakari:first-class-newtypes

Conversation

@papa-zakari
Copy link
Copy Markdown

@papa-zakari papa-zakari commented Apr 18, 2026

This PR proposes the addition of first class newtype support. This change would provide a discrete abstraction to help unload the overloaded tuple struct.


Rendered


tldr;

// The forwarding impls that we'll generate for Id are: Display + FromStr. 
#[derive(Clone, Copy, Display, Debug, FromStr, PartialEq, PartialOrd)]
pub newtype Id = u64;

// can this trait be derived? yes
//     -> derive as usual (built-in or macro)
// 
// can this trait be derived? no
//    ┌ is the derive attr on a newtype? yes
//    │     does the original type implement this trait? yes
//    │        -> generate a forwarding impl
//    │
//    │
// newtype is a qualifier for deriving forwarding impls for the "original" or
// inner type of a single field tuple struct.
// 
// #[repr(transparent)] is opt-in as it takes away from the opaque-ness (opacity)
// of a newtype. Pun not intended.

println!("{:?}", Id(123);
// => Id(123)     

println!("{}", Id(123));
// => 123

@RustyYato
Copy link
Copy Markdown

RustyYato commented Apr 18, 2026

I have a few questions:

  1. Did you use an AI to write this RFC?
  2. How would these derives work?
#[derive(Add, Mul)]
newtype Meter = i32;

Note here that Meter * Meter shouldn't output Meter, so deriving Mul<Self> is wrong, we want Mul<i32>. But Meter + Meter is perfectly sane, and in fact what we want. So deriving Add<Self> is correct, and Add<i32> would be wrong.

So there are conflicting rules on how forwarding should work, even for std traits. How are you going to resolve this conflict?

  1. How does this interact with delegation RFCs? Implement function delegation in rustc #3530

@9291Sam
Copy link
Copy Markdown

9291Sam commented Apr 19, 2026

What is the intention of this PR? It seems to be predicated around the idea that types are comparable in some way, yet they are not. Additionally, it seems to imply that this would be solving a problem that the language currently has. However, the implied problem (that traits are implemented transitively on new types) is not real.

I entirely fail to see the issue this PR is hopes to solve, it's proposed solution, and what, if accepted, this PR would actually do to the language. And, I do not seem to be alone in this view.

Can you please elaborate as to what the intention of this PR is?

@papa-zakari
Copy link
Copy Markdown
Author

@RustyYato

I have a few questions:

  1. Did you use an AI to write this RFC?

Yes, I used ChatGPT. I asked them to write an RFC in the style of nikomatsakis and withoutboats after rubber ducking the concept. I think they did a pretty good job.

  1. How would these derives work?
#[derive(Add, Mul)]
newtype Meter = i32;

Note here that Meter * Meter shouldn't output Meter, so deriving Mul<Self> is wrong, we want Mul<i32>. But Meter + Meter is perfectly sane, and in fact what we want. So deriving Add<Self> is correct, and Add<i32> would be wrong.

So there are conflicting rules on how forwarding should work, even for std traits. How are you going to resolve this conflict?

A newtype is a fundamentally different type from it's original type. The rules for auto-deriving traits for a newtype are not yet defined.

In rustc today, if you try to derive a trait that is not derivable you get an error message mentioning that there is not a derive macro in scope. If it were a newtype, it would first try to resolve the impl on the original type and determine if it is derivable based on the set of rules that we define.

The rules we define can be something as naive as excluding over-loadable operators or something slightly more complicated and strict such as excluding any trait as derivable for a newtype if the trait has generic arguments or associated types. I personally prefer the latter. This helps define the boundary around newtype construction and type casts from the original type.

  1. How does this interact with delegation RFCs? Implement function delegation in rustc #3530

It's rather common to manually delegate to an individual function on an impl for the inner type when conceptually implementing a newtype with a tuple struct. Function delegation support in rustc could be a solution for developers that wish to restrict the capability of a newtype's original type without having to separate behavior into a separate trait. This is particularly beneficial for library authors that want a trait to have semantic meaning in addition to being a means of code reuse.

IMO, having newtype as a language construct provides an alternative syntax for beginners that may be inclined to #[derive(Deref)]. Having such an alternative could be prerequisite to implementing rust-lang/rfcs#3911 to prevent simulated inheritance via Deref from becoming a bigger problem than the status quo.

@papa-zakari
Copy link
Copy Markdown
Author

papa-zakari commented Apr 20, 2026

@9291Sam

What is the intention of this PR? It seems to be predicated around the idea that types are comparable in some way, yet they are not. Additionally, it seems to imply that this would be solving a problem that the language currently has. However, the implied problem (that traits are implemented transitively on new types) is not real.

I entirely fail to see the issue this PR is hopes to solve, it's proposed solution, and what, if accepted, this PR would actually do to the language. And, I do not seem to be alone in this view.

Can you please elaborate as to what the intention of this PR is?

This RFC is a formalization of a pattern that already exists in Rust today. The newtype pattern is not unique to Rust. Both Haskell and flow (a static type checker for JavaScript) have newtype as a first class language construct. These implementations have been considered for the (ongoing) design of this RFC and can also inform the implementation.

Formalizing this pattern reduces temptation for Rust developers to impl Deref for a tuple struct wrapper as a means of sharing behavior of two types that have a different semantic meaning. A real feature gap that contributes to an anti-pattern.

Unlike a type alias, newtype must be nominally different. Similar to a type alias newtype can be structurally identical to the original type.

The simplest way we could implement newtype would desugar to a single field tuple struct with #[repr(transparent)]. Structurally identical to the inner type but nominally different.

The newtype keyword shall be used as a qualifier that determines whether or not an impl on the original type can be derived for the newtype.

Similar to how async is a qualifier for .await, use a variation of the resolution algorithm for determining whether or not an identifier can appear in the list of arguments passed to the attribute #[derive(<list>)].

#[repr(transparent)]
struct Meter(i32);

@ehuss ehuss added T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC. labels Apr 20, 2026
newtype ProtectFromForgery = Identity;
```

This creates a distinct type:
Copy link
Copy Markdown
Member

@kennytm kennytm Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given an Identity value how do you construct a ProtectFromForgery value?

and in the opposite direction, given a ProtectFromForgery how do recover the Identity (by move / by ref / by mut ref)?

how to control which module can do either operation?

in the existing "new type" pattern these can all done with existing syntax

// member is private, so only accessible in the current module.
pub struct ProtectFromForgery(Identity);

// wrapping
let protected = ProtectFromForgery(identity);
// unwrapping
let identity = &protected.0;

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting question. I like the idea of primitive type casts working for newtypes regardless of the original type (with constraints).

However, there are two problems that come when allowing primitive casts to and from newtype / "original".

  1. Visibility rules would have to apply to the primitive "style" type cast in that it would only be safe to do in the module in which the newtype is defined. This is similar to how opaque type works in flow.

  2. A variation of a primitive type cast as _ that is always valid could desensitize code reviewers that naturally pay more attention to an expression that includes as.

A comma following an ident after the equal sign of a type is a syntax error. Tuple struct declaration and constructor syntax allow developers to specify additional fields. If we assume that newtype should use tuple struct syntax, lexing may require more context than what is available to properly fail if more than one field is specified for a tuple struct.

A primitive type cast on the hand can fail when types are checked. If visibility is a part of type checking in rustc, an opaque type style constructor syntax would actually be easier implement and doesn't require any changes to Rust's grammar.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mod id {
    // Conversion or constructor fns are the only way to create an Id outside of mod id. 
    #[derive(FromStr)]
    pub newtype Id = u64;
    impl Id {
        pub fn new(id: u64) -> Id { id as Id } // Ok
        //                             ^^^^^
        // Explicit type cast required in order to construct Id.
    }

}

use id::Id;
use std::num::ParseIntError;

fn main() {
    let _: Result<Id, ParseIntError> = "123".parse(); // Ok
    let _ = 123 as Id; // Error non-primitive cast: i32 as Id
}

Comment on lines +97 to +101
This differs from `#[derive]` on structs in that:

* it is not limited to compiler-known traits
* it can apply to user-defined traits
* it can generate forwarding impls to the inner type
Copy link
Copy Markdown
Member

@kennytm kennytm Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a big departure from how #[derive(X)] today works on every other item, as that X is actually a macro name, not necessarily a trait name (e.g. CoercePointee).

why this can't be like

#[derive(Clone)]
#[forward_traits_for_new_type(Token)]
pub struct ProtectFromForgery(Identity);

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kennytm a different attribute from derive is a good way to disambiguate what is a forwarding impl vs. a trait derived from a macro for the newtype. This is a reasonable alternative to adding a new keyword to the language.

I went ahead and added a tldr; to description beneath the rendered RFC. It explains my thinking around forwarding impls safely coexisting alongside custom derives and derivable traits in std.

All of that falls apart if this becomes an RFC for deriving forwarding impls for single field tuple structs. And for what it's worth, I'm perfectly fine with that. Even if I personally prefer a qualifying keyword over an additional attribute that works like derive but with different rules.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@papa-zakari making #[derive]'s generated interface behave differently is a deal breaker IMO. For instance the macro driving #[derive(PartialEq)] will make the type derive StructuralPartialEq too, but if the meaning is changed to just forwarding the trait this behavior would be lost, and this would affect whether const of such type can be used in match and const-generics.

And if you use a spelling other than #[derive(Token)], I see no reason to waste a contextual keyword as #[forward_traits_for_new_type(Token)] struct Foo(Bar) can work as well.

@apiraino
Copy link
Copy Markdown

Yes, I used ChatGPT. I asked them to write an RFC in the style of nikomatsakis and withoutboats after rubber ducking the concept. I think they did a pretty good job.

@papa-zakari Hi, this is the moderation team of the Rust project. It looks like you are using an LLM without reviewing its output sufficiently. This is against our contribution guidelines and we have to be strict about that because we have limited review bandwidth.

Please ensure you're familiar with our contribution guidelines, specifically our etiquette and feel free to either reach out to moderation or ask on the Zulip stream for general help from other contributors.

@papa-zakari
Copy link
Copy Markdown
Author

@apiraino I read through the contributing guidelines and I'm unfortunately struggling to see where the violation is. Can you point it out specifically? I'm happy to make the correction so we can move forward with this.

@papa-zakari
Copy link
Copy Markdown
Author

This has come up a couple of times already. This RFC has no opinion about representation. Layout can be different unless otherwise specified. No different from a tuple struct.

@apiraino
Copy link
Copy Markdown

@apiraino I read through the contributing guidelines and I'm unfortunately struggling to see where the violation is. Can you point it out specifically? I'm happy to make the correction so we can move forward with this.

The relevant point is that we ask contributors to "fully own their contributions". We are under a constant influx of machine-generated patches/issues and we are trying to make it clear that we don't have capacity to evaluate poorly authored/low-effort contributions. Asking a machine to generate text parroting other Rust contributors raised some questions about the effort that went into authoring this proposal; we want to be sure we are talking with papa-zakari, not with nikomatsakis or withoutboats. Not saying that this is necessary the case here, we want to assume that you fully understand the proposal.

Oh and probably a relevant similar issue might be #2242

(this clarification is not meant to derail the technical discussion. For further clarifications specifically on this, feel free to reach out to the mods. We are in the process of drafting guidelines about LLM-guided contributions so I don't have an exact policy to point you at right now but we might close low-effort contributions as per rust-lang/compiler-team#893)

Copy link
Copy Markdown
Contributor

@clarfonthey clarfonthey Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd been holding off on commenting on this mostly because I wasn't sure what the moderation plan was going to be, but since we seem to be leaving it open for now, I might as well add some comments on the technical merits of this.

This proposal mostly seems to me like an uncritical adoption of golang's newtype system without really trying to understand why it and Rust have different approaches to this. I think that there is definitely room for improvement; one example is how we seem to be going in the direction of allowing literals to coerce to newtypes like NonZero, and we might ultimately allow more examples of this in the future.

Similarly, the ability to "delegate" trait implementations through the newtype are also something that is desired and being worked on. An obvious example of this is the Iterator trait, since delegation not only makes things easier but directly improves performance: unless you manually delegate every method, then iterators with specialised method bodies will be ignored and fall back to their generic versions.

Ultimately, there are lots of potential places for incremental improvement here, but it's unlikely that any of them will involve carving out a dedicated syntax for newtypes like this. In particular, the justification for adding a new context-sensitive keyword newtype seems relatively weak.

View changes since the review

Copy link
Copy Markdown
Contributor

@clarfonthey clarfonthey Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening a separate thread for my opinion on this RFC as a contribution itself, to maybe clarify why simply using an LLM to write it is an issue.

First, and this is something that could probably been easily fixed, it does not fully follow the RFC template. I don't know if this is ChatGPT's fault or yours, but past the "guide-level explanation" header, none of the remaining template is followed. We have this template for a reason and ignoring it generally means that an idea hasn't been fleshed out enough to the point where it can be seriously discussed. In particular, the reference-level explanation is critical for explaining how this feature could actually be implemented, and the prior art section is critical for verifying that you've looked into existing proposals, parallel implementations, etc. to verify that this feature is in fact justified.

Second, you also didn't follow the PR template, which includes a note suggesting to create separate threads on RFCs like I'm doing right now. This is a simple mistake, but it's one of multiple mistakes, which is why I'm pointing it out. You can easily just copy the block from another RFC or the PR template manually.

And finally, this is more an opinion of my own and I can't comment on how Niko and withoutboats feel on the matter, but I personally would feel quite upset if someone explicitly tried to write an RFC in "my style," and considering how LLMs operate by interpolating existing works, it can sometimes run the line extremely close to plagiarism. While we still haven't formed a proper policy on LLMs yet, I think that in general even if you're still using them to draft things, you should be attempting to form your own style, not copy someone else's.

It's important to understand what makes a good RFC, and not to simply do what other people are doing. We also change the process regularly to incorporate feedback, and copying existing work can mean these changes and their motivations get lost.

View changes since the review


---

## Example: HTTP session extension pattern
Copy link
Copy Markdown

@lebensterben lebensterben Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry this section and the next one (Separation of behavior and data) doesn't make any sense to me and are confusing.

View changes since the review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants