Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 308 additions & 0 deletions text/3951-first-class-newtypes.md
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

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:
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
}


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


---

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


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.