-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: initial proposal for first class newtypes #3951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| - Feature Name: (`first_class_newtypes`) | ||
| - Start Date: (2026-04-18) | ||
| - RFC PR: [rust-lang/rfcs#3892](https://github.com/rust-lang/rfcs/pull/3951) | ||
| - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
|
||
| ## Summary | ||
|
|
||
| This RFC proposes first-class language support for *nominal newtypes*, providing a lightweight mechanism to create distinct types from existing types with controlled trait propagation, optional structural transparency, and improved ergonomics over tuple structs and proc-macro wrappers. | ||
|
|
||
| The goal is to make the "newtype pattern" a first-class abstraction rather than a purely idiomatic convention, while preserving Rust’s emphasis on explicitness, zero-cost abstractions, and predictable trait coherence. | ||
|
|
||
| --- | ||
|
|
||
| ## Motivation | ||
|
|
||
| Rust’s current story for creating distinct types around existing representations relies on tuple structs: | ||
|
|
||
| ```rust | ||
| #[derive(Clone)] | ||
| pub struct Identity([u8; 16]); | ||
|
|
||
| #[derive(Clone)] | ||
| struct ProtectFromForgery(Identity); | ||
| ``` | ||
|
|
||
| This works, but has several limitations: | ||
|
|
||
| 1. **Boilerplate and inconsistency** | ||
|
|
||
| * Every newtype requires manual forwarding of traits. | ||
| * Derive works only for a subset of traits (Copy, Clone, Debug, etc.). | ||
| * Behaviorally meaningful traits must be manually implemented or macro-generated. | ||
|
|
||
| 2. **No structured control over shared behavior** | ||
|
|
||
| * Traits on the inner type are not automatically considered. | ||
| * Deref makes sense for smart pointers but it is frequently seen as an anti-pattern. | ||
|
|
||
| 3. **Cross cutting concerns of smart pointers and type wrappers** | ||
|
|
||
| * Newtypes are used for: | ||
| * semantic typing `Kilometers = i32` | ||
| * capability filtering | ||
| * API contracts that depend on type system visibility | ||
| * but the language encourages us to treat them like a smart pointer. | ||
|
|
||
| This RFC proposes treating newtypes as a *first-class nominal abstraction*, enabling both stronger guarantees and better ergonomics. | ||
|
|
||
| --- | ||
|
|
||
| ## Guide-level explanation | ||
|
|
||
| A **newtype** is a distinct type created from an existing type, with optional rules governing: | ||
|
|
||
| * representation (transparent or opaque) | ||
| * derived behavior | ||
| * capability restriction | ||
|
|
||
| ### Proposed syntax | ||
|
|
||
| A newtype declaration resembles a type alias, but introduces a nominal type: | ||
|
|
||
| ```rust | ||
| newtype ProtectFromForgery = Identity; | ||
| ``` | ||
|
|
||
| This creates a distinct type: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given an and in the opposite direction, given a 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;
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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 A primitive type cast on the hand can fail when types are checked. If visibility is a part of type checking in rustc, an
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
} |
||
|
|
||
| ```rust | ||
| ProtectFromForgery != Identity | ||
| ``` | ||
|
|
||
| By default, this type is **opaque**, but may opt into representation transparency: | ||
|
|
||
| ```rust | ||
| #[repr(transparent)] | ||
| newtype ProtectFromForgery = Identity; | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Trait behavior | ||
|
|
||
| One of the central design questions is trait propagation. | ||
|
|
||
| This RFC proposes **opt-in derives**, rather than implicit blanket forwarding. | ||
|
|
||
| ### Explicit derivation | ||
|
|
||
| Traits may be derived on the newtype: | ||
|
|
||
| ```rust | ||
| #[derive(Clone, Token)] | ||
| newtype ProtectFromForgery = Identity; | ||
| ``` | ||
|
|
||
| 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 | ||
|
Comment on lines
+97
to
+101
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a big departure from how why this can't be like #[derive(Clone)]
#[forward_traits_for_new_type(Token)]
pub struct ProtectFromForgery(Identity);
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @papa-zakari making And if you use a spelling other than |
||
|
|
||
| --- | ||
|
|
||
| ## Key design question: automatic trait resolution | ||
|
|
||
| A central unresolved question is: | ||
|
|
||
| > Should impls on the inner type automatically apply to the newtype? | ||
|
|
||
| ### Option A: No automatic resolution (recommended default) | ||
|
|
||
| ```text | ||
| Identity implements Token | ||
| ProtectFromForgery does NOT | ||
| ``` | ||
|
|
||
| Pros: | ||
|
|
||
| * preserves capability restriction guarantees | ||
| * avoids accidental API leakage | ||
| * aligns with Rust’s explicitness philosophy | ||
|
|
||
| Cons: | ||
|
|
||
| * requires boilerplate or derives | ||
|
|
||
| --- | ||
|
|
||
| ### Option B: Full transparent inheritance | ||
|
|
||
| All impls on `Identity` are visible on `ProtectFromForgery`. | ||
|
|
||
| Pros: | ||
|
|
||
| * maximal ergonomics | ||
| * zero boilerplate | ||
|
|
||
| Cons: | ||
|
|
||
| * destroys the primary value of newtypes (capability boundary) | ||
| * makes reasoning about APIs difficult | ||
| * blurs nominal distinction | ||
|
|
||
| --- | ||
|
|
||
| ### Option C: Allow auto-deriving traits for a newtype (proposed) | ||
|
|
||
| This is the safest option as it requires the smallest amount of new syntax. | ||
| Reserving `newtype`. | ||
|
|
||
| This preserves: | ||
|
|
||
| * explicit capability boundaries | ||
| * ergonomic forwarding | ||
| * predictable trait resolution | ||
|
|
||
| This is the recommended design direction. | ||
|
|
||
| We can emulate it today with a procedural macro and trait resolution rules do | ||
| not have to change. | ||
|
|
||
| ```rust | ||
| newtype! { | ||
| #[derive(Clone, Token)] | ||
| type ProtectFromForgery = Identity; | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Representation | ||
|
|
||
| A newtype may optionally guarantee identical layout: | ||
|
|
||
| ```rust | ||
| #[repr(transparent)] | ||
| newtype ProtectFromForgery = Identity; | ||
| ``` | ||
|
|
||
| This guarantees: | ||
|
|
||
| * identical ABI representation | ||
| * safe transmutation (where allowed) | ||
| * FFI compatibility | ||
|
|
||
| Without `repr(transparent)`, layout is an implementation detail. | ||
|
|
||
| --- | ||
|
|
||
| ## Example: HTTP session extension pattern | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| This RFC targets a common Rust pattern: request-local state stored in extensions. | ||
|
|
||
| Current pattern: | ||
|
|
||
| ```rust | ||
| self.extensions() | ||
| .get::<ProtectFromForgery>() | ||
| ``` | ||
|
|
||
| With newtypes: | ||
|
|
||
| ```rust | ||
| newtype ProtectFromForgery = Identity; | ||
| ``` | ||
|
|
||
| Trait separation becomes clearer: | ||
|
|
||
| ```rust | ||
| pub trait Session { | ||
| fn session(&self) -> Option<&Identity>; | ||
| } | ||
| ``` | ||
|
|
||
| Capability restriction is enforced at the type level rather than runtime casting. | ||
|
|
||
| --- | ||
|
|
||
| ## Separation of behavior and data | ||
|
|
||
| This RFC strongly encourages a separation model: | ||
|
|
||
| * **data types**: represent state | ||
| * **traits**: represent capability | ||
|
|
||
| Newtypes act as a *boundary layer* between them. | ||
|
|
||
| Example: | ||
|
|
||
| ```rust | ||
| newtype Identity = [u8; 16]; | ||
|
|
||
| pub trait Token { | ||
| fn expires_at(&self) -> Result<i64, Error>; | ||
| } | ||
| ``` | ||
|
|
||
| This makes it possible to: | ||
|
|
||
| * restrict Token behavior to specific contexts | ||
| * avoid accidental API surface expansion | ||
| * encode domain constraints explicitly | ||
|
|
||
| --- | ||
|
|
||
| ## Drawbacks | ||
|
|
||
| ### 1. Increased type system complexity | ||
|
|
||
| Depending on how newtypes are implemented, trait resolution can become a challenge. | ||
|
|
||
| ### 2. Potential confusion with aliases | ||
|
|
||
| Users may conflate: | ||
|
|
||
| * `type X = Y;` | ||
| * `newtype X = Y;` | ||
|
|
||
| Clear syntax differentiation is required. | ||
|
|
||
| ### 3. Trait resolution complexity | ||
|
|
||
| The interaction between: | ||
|
|
||
| * blanket impls | ||
| * orphan rules | ||
| * derive | ||
|
|
||
| requires careful compiler design. | ||
|
|
||
| --- | ||
|
|
||
| ## Alternatives considered | ||
|
|
||
| ### 1. Continue using tuple structs | ||
|
|
||
| Rejected due to boilerplate and lack of expressive control. | ||
|
|
||
| ### 2. Expand derive system only | ||
|
|
||
| Insufficient for user-defined traits and capability control. | ||
|
|
||
| ### 3. Standardize macro-based newtypes | ||
|
|
||
| Already partially solved in ecosystem, but inconsistent and non-semantic. | ||
|
|
||
| --- | ||
|
|
||
| ## Open questions | ||
|
|
||
| 1. Should we allow deriving `Deref` if [rust-lang/rfcs#3911](https://github.com/rust-lang/rfcs/pull/3911) is accepted? | ||
| 2. Should newtypes have a way of applying impl blocks from their inner type? | ||
| 3. How do we teach beginners about the difference between a tuple struct and newtype? | ||
|
|
||
| --- | ||
|
|
||
| ## Future work | ||
|
|
||
| * integration with const generics for parameterized newtypes | ||
| * potential linting around overuse of extensions maps | ||
| * ergonomic sugar for request-scoped capability wrappers | ||
|
|
||
| --- | ||
|
|
||
| ## Closing thought | ||
|
|
||
| This proposal attempts to formalize what is currently an idiomatic but fragmented pattern in Rust. The goal is not to reduce explicitness, but to make *explicit structure easier to express than accidental structure*, particularly in systems where type-based capability control is a core part of correctness. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
Iteratortrait, 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
newtypeseems relatively weak.View changes since the review