Skip to content

RFC: enable derive(From) for single-field structs #3809

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

Kobzol
Copy link
Member

@Kobzol Kobzol commented May 6, 2025

Previously discussed as Pre-RFC on IRLO.

Rendered

@jieyouxu jieyouxu added the T-lang Relevant to the language team, which will review and decide on the RFC. label May 6, 2025
@clarfonthey
Copy link

Fully in support of this. A few things that probably should be elaborated:

  • Generic single fields
  • Transparent structs with extra zero-size fields like PhantomData

The transparent case, IMHO, is probably best left as a future extension, but it's an interesting case where Default impls on the extra fields would not be needed.

@Kobzol
Copy link
Member Author

Kobzol commented May 6, 2025

Fully in support of this. A few things that probably should be elaborated:

  • Generic single fields
  • Transparent structs with extra zero-size fields like PhantomData

The transparent case, IMHO, is probably best left as a future extension, but it's an interesting case where Default impls on the extra fields would not be needed.

PhantomData implements Default unconditionally for any T, so that would be fine. I agree it's best to leave it for "later" though.

Good point with the generics, I'll add a mention to the RFC.

@kennytm
Copy link
Member

kennytm commented May 7, 2025

"For later" - perhaps it could recognize #3681 fields and automatically skip them as well.

#[derive(From)]
struct Foo {
    a: usize, // no #[from] needed, because all other fields are explicitly default'ed
    b: ZstTag = ZstTag,
    c: &'static str = "localhost",
}

// generates

impl From<usize> for Foo {
    fn from(a: usize) -> Self {
        Self { a, .. }
    }
}

OTOH we may still want the #[from] in the above case for uniformity if we want to support #[derive(From)] on all fields being defaulted

#[derive(From, Default)]
struct Foo2 {
    #[from] // <-- perhaps still want this
    a: u128 = 1,
    b: u16 = 8080,
}

Co-authored-by: Jake Goulding <[email protected]>
@nielsle
Copy link

nielsle commented May 9, 2025

The crate named derive-new creates a new constructor, and it has several fancy options.

But perhaps this functionality belongs in a third party crate rather than the standard library.


We could make `#[derive(From)]` generate both directions, but that would make it impossible to only ask for the "basic" `From` direction without some additional syntax.

A better alternative might be to support generating the other direction in the future through something like `#[derive(Into)]`.
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worth noting here that impl From<Newtype> for Inner and impl Into<Inner> for Newtype have slightly different semantics. Using derive(Into) to mean impl From could be confusing for generic types where there is a coherence issue, even if it does imply an Into impl.

(I've had similar issues with the derive_more version.)

Copy link
Member

Choose a reason for hiding this comment

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

the derive-macro's name does not necessarily correspond to the impl'ed trait name anymore since #3621 (currently named #[derive(CoercePointee)], which actually does impl CoerceUnsized + DispatchFromDyn.)

Copy link
Member Author

Choose a reason for hiding this comment

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

The #[derive(Into)] example is kind of hand-waving, I'm not sure if it's actually a good idea. I would like to avoid doing that in this RFC though, as that sounds like a separate can of worms, I explicitly tried to keep this RFC as simple as possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

the derive-macro's name does not necessarily correspond to the impl'ed trait name anymore since #3621 (currently named #[derive(CoercePointee)], which actually does impl CoerceUnsized + DispatchFromDyn.)

Understood! But my comment was more about the confusing semantics, than the exact name of the impl'd trait.

Choose a reason for hiding this comment

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

Honestly, for the impl From<NewType> for Inner case, I think I would prefer something like impl_from!(Newtype -> Inner) or a more generic impl_from!(Newtype -> Inner = |source| source.0) or impl_from!(Newtype -> Inner = self.0) (restricted to using fields of NewType)

Although it is a little annoying, to have to repeat the type of Inner.

Copy link
Member

Choose a reason for hiding this comment

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

But my comment was more about the confusing semantics

I don't think there is any name more appropriate than #[derive(Into)] especially if this RFC is using #[derive(From)]. The final effect you get is still having an impl Into<Inner> for Self effectively, through the intermediate impl From<Self> for Inner + the blanket impl.

See JelteF/derive_more#13 for a brief discussion how derive_more still chooses to name it #[derive(Into)].

Copy link
Member

Choose a reason for hiding this comment

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

I'd love to have a derive in the other direction, but full support for making that future work and not part of this RFC.

@Kobzol
Copy link
Member Author

Kobzol commented May 10, 2025

The crate named derive-new creates a new constructor, and it has several fancy options.

But perhaps this functionality belongs in a third party crate rather than the standard library.

While new constructors are pretty common, I would say that it's too opinionated for it to be included in the stdlib, as it's not a standard (library) trait like From is. In any case, that would be a separate RFC :)

Co-authored-by: teor <[email protected]>
@joshtriplett joshtriplett added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label May 28, 2025
@joshtriplett
Copy link
Member

I think this proposal makes sense as written, and it's simple and straightforward.

Should we allow derive(From) on single-field structs, to impl From<FieldType> for Struct?

@rfcbot merge

@rfcbot
Copy link
Collaborator

rfcbot commented May 28, 2025

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels May 28, 2025
@traviscross traviscross added the P-lang-drag-2 Lang team prioritization drag level 2. label May 28, 2025
@matthieu-m
Copy link

A better alternative might be to support generating the other direction in the future through something like #[derive(Into)].

I very much encourage distinct syntax for distinct directions.

In particular, as noted in the Email(String) example, newtype wrappers are regularly used to enforce invariants, in which case the #[derive(From)] implementation is outright wrong.

On the other hand, going back to the inner type should never violate any invariant, and therefore #[derive(Into)] makes perfect sense for Email.

@scottmcm
Copy link
Member

Unsure: is this really lang, @joshtriplett? It's an impl that could be done in a stable proc macro crate, AFAICT, which to me says that it's entirely libs-api (even if in actual implementation it'd be done inside the compiler today).


Personally I'm not concerned with "well this From might be wrong", because correctness is up to the caller to do properly (like how today derive(Clone) might be wrong if you're holding pointers or how derive(Hash) might be wrong if you implemented PartialEq manually.) I think in a hypothetical future with unsafe fields then perhaps this should fail, but that's not something this RFC would need to mention because there's no accepted RFC for that yet. (Though if it wanted to discuss that as future possibilities that would be nice-to-have.)

@traviscross
Copy link
Contributor

traviscross commented Jun 12, 2025

As a comparable, we handled RFC #3107 (#[default]) as a dual lang/libs-api FCP. Probably I think that makes sense here as well (e.g., it maybe leads to #[from]). This perhaps touches on general "flavor of the language" matters, and also raises for us questions about whether we might like to solve the motivation here in some other language-directed way.

@rfcbot fcp cancel
@rustbot labels +T-libs-api

@rfcbot
Copy link
Collaborator

rfcbot commented Jun 12, 2025

@traviscross proposal cancelled.

@rustbot rustbot added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Jun 12, 2025
@rfcbot rfcbot removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Jun 12, 2025
@traviscross
Copy link
Contributor

@rfcbot fcp merge

(And I'll recheck the box for Josh.)

@rfcbot
Copy link
Collaborator

rfcbot commented Jun 12, 2025

Team member @traviscross has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Jun 12, 2025
@traviscross
Copy link
Contributor

Probably I should have added, "...and it seems maybe more straightforward to just do a proposed dual FCP rather than delving deeply into the jurisdictional question."

@clarfonthey
Copy link

Considering how straightforward and supported this RFC is, it probably would take longer to litigate which teams should review the RFC compared to just asking both teams to check their boxes.

Enabling this would make one more intuitive use-case in the language "just work", and would reduce boilerplate that Rust users either write over and over again or for which they have to use macros or external crates.

## Newtype pattern
As a concrete use-case, `#[derive(From)]` is particularly useful in combination with the very popular [newtype pattern](https://doc.rust-lang.org/rust-by-example/generics/new_types.html). In this pattern, an inner type is wrapped in a new type (hence the name), typically a tuple struct, to semantically make it a separate concept in the type system and thus make it harder to mix unrelated types by accident. For example, we can wrap a number to represent things like `Priority(i32)`, `PullRequestNumber(u32)` or `TcpPort(u16)`.
Copy link
Member

@RalfJung RalfJung Jun 12, 2025

Choose a reason for hiding this comment

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

This is a pretty odd first motivation to list, since by having a From instance, the newtype does not actually represent an independent concept any more. In particular, if there are any invariants on the newtype, a From instance must not be used. And even if the goal is just "separation of concepts", a From instance seriously subverts this by making it easy to accidentally convert the field type to the newtype without even realizing that one is crossing a concept barrier.

Given that newtpyes with invariants are common, I think the text here at least needs to be more explicit that it is only talking about newtypes where the field is effectively public anyway.

Deriving Into would be a lot more useful for newypes, IMO...

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that newtypes have several benefits that gradually provide more guarantees.

The first one (and in my view, the primary one, which is provided by all uses of a newtype) is to introduce a new entity in the type system. That's literally what the pattern says - newtype - introduce a new type. This, on its own, is incredibly useful, to avoid mixing unrelated things. I use it all the time, e.g. TaskId, WorkerId, UserId, all distinct types that represent all values of e.g. a u32. It's perfectly fine to implement From for these newtypes. So I think that the derive is very useful for this use-case.

Then, as an additional invariant, you can say that your newtype supports only a subset of values of the inner field. In that case, you of course shouldn't implement From, but have a custom fallible constructor.

Copy link
Member

Choose a reason for hiding this comment

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

The first one (and in my view, the primary one, which is provided by all uses of a newtype) is to introduce a new entity in the type system. That's literally what the pattern says - newtype - introduce a new type. This, on its own, is incredibly useful, to avoid mixing unrelated things. I use it all the time, e.g. TaskId, WorkerId, UserId, all distinct types that represent all values of e.g. a u32.

I'm with you until here.

It's perfectly fine to implement From for these newtypes.

You lost me here. This seems to negate a large chunk of the benefits of making this a new type. Now it is very easy to take a random integer from $somewhere and turn it into a TaskId without even realizing that is what happens. By adding a From impl, you lose the enforced abstraction, and by adding impl Into<TaskId> functions you've lost nearly all benefits of this pattern.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, it turns out the newtypes in the compiler do implement From:
https://doc.rust-lang.org/nightly/nightly-rustc/rustc_abi/struct.FieldIdx.html

That is quite surprising, I wonder why that was done.

Copy link
Member

Choose a reason for hiding this comment

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

So maybe I am the odd one out with my strict use of newtypes? In many cases this seems to be used as Newtype::from(...), which I am entirely fine with, but I did find a few foo.into() relying on this as well and that's not something I would do in my codebases.

FWIW, Miri's newtype macro does not add a From, precisely because I never even considered that to be a reasonable thing to do with newtypes.^^

Copy link
Member

Choose a reason for hiding this comment

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

I see. So you're fine with having a constructor like Newtype::new or Newtype::from, but you don't like .into(), got it.

Yes, exactly. :) I would make from an inherent method of the type, rather than getting it via impl From, but then one can only convert from a single type so one often ends up with from_foo and from_bar which is somewhat annoying... sometimes one can get away with from(x: impl Into<Something>), but that does not always work.

Copy link

@teohhanhui teohhanhui Jun 12, 2025

Choose a reason for hiding this comment

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

As a user, why would I not want .into()? It is good for ergonomics. Unless your goal is to annoy the callers as much as possible into using your preferred calling style.

The API promise is that if there's impl From<A> for B, then B can be constructed from A infallibly. Why should the callee get to dictate: you must write B::from(a), but not let b: B = a.into()? It seems like overreach. (And if that's really your goal in a project, it can probably still be enforced via clippy lints, right?)

Copy link
Member

Choose a reason for hiding this comment

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

As a user, why would I not want .into()?

I've explained this above. In my view, it is "good for ergonomics" the same way not having a type system is "good for ergonomics" (that's way more extreme, of course, but you get the point).

Unless your goal is to annoy

You are not commenting in good faith. Please behave better than that.

Copy link

@teohhanhui teohhanhui Jun 12, 2025

Choose a reason for hiding this comment

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

In my view, it is "good for ergonomics" the same way not having a type system is "good for ergonomics" (that's way more extreme, of course, but you get the point).

I think what you're arguing for is akin to "type inference considered harmful", which is what Into is about, right?

You are not commenting in good faith. Please behave better than that.

That's being taken out of context by cutting off half of the sentence.

Copy link
Member

@RalfJung RalfJung Jun 12, 2025

Choose a reason for hiding this comment

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

No, I love type inference. I also love abstractions. And having var.into() convert a u32 into an Idx silently breaks the abstraction since it is not explicit that I am creating an Idx here.

I explained this above already. I won't reply further unless you actually add something new, or ask a question that has not yet been answered.

Comment on lines 54 to 55
### Simplifying test code
Apart from the `From` trait being generally useful in various situations, it is especially handy in tests, where various fixture functions can simply receive `T: Into<Newtype>` to avoid test code having to use `.into()` or a newtype struct constructor everywhere.
Copy link
Member

Choose a reason for hiding this comment

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

If an API does this I question the use of a newtype makes any sense. The entire point is to avoid accidental mixups, but then these kinds of functions completely subvert that point.

Are there real-world examples of newtypes doing this?

Copy link
Member Author

@Kobzol Kobzol Jun 12, 2025

Choose a reason for hiding this comment

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

There is nothing implicit about this. I still need to call .into() to convert u32 to the newtype. Even in generic functions taking Into, I can't pass a value of NewtypeB by accident. For that, the function would need to take something like X: Into<u32> and then you'd need to call .into().into() on that to "accidentally" convert from NewtypeB to NewtypeA.

One more point about implicitness, implementing From for a newtype of course makes it a bit easier to go from the inner type to the newtype (it's still not implicit though! and I explicitly mentioned this being usedbin test code, where I want to avoid boilerplate). But it doesn't make it easier to go from NewtypeA to NewtypeB, which I see as the main important thing. If something is u32, it's not very "type safe" anyway.

A real-world example are essentially all helper functions that I write in my tests :D

Copy link
Member

@RalfJung RalfJung Jun 12, 2025

Choose a reason for hiding this comment

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

It is fully implicit: foo.do_task(13) will just work if do_task takes an Impl<TaskId> and there is a derived From.

I don't think test code is the best argument for an RFC. We shouldn't optimize for quick-and-dirty throw-away code as we often see it in a test suite.

you'd need to call .into().into() on that to "accidentally" convert from NewtypeB to NewtypeA.

That seems entirely realistic, I've thrown into() into my code until it works when dealing with str/String/OsString/PathBuf/... and the compiler can even recommend adding .into() which tools can then auto-apply.

Copy link
Member

Choose a reason for hiding this comment

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

I checked the compiler and could not find a use of impl Into for a newtype like this. Phew. ;)

I admit I overlooked the part where it said "in tests" here when I first read the RFC. Still, I don't think the RFC should be arguing for this pattern. Most of the time, and certainly in the public API of a crate, I'd consider this an anti-pattern. (I haven't seen your test code, I could imagine that it is sufficiently local and otherwise explicit that the benefits outweigh the downsides.) Anti-patterns don't make for great motivation for new features.

But it doesn't make it easier to go from NewtypeA to NewtypeB, which I see as the main important thing.

If NewtypeA implements Into<u32> (which I consider to be entirely reasonable), then a.into() could now be passed to an impl Into<NewtyeB> function and the conversion would be very implicit. Or does that break due to inference variable ambiguity?

If something is u32, it's not very "type safe" anyway.

Agreed. That's why it should not be possible to call do_task(some_u32).

@RalfJung
Copy link
Member

RalfJung commented Jun 12, 2025

Personally I'm not concerned with "well this From might be wrong", because correctness is up to the caller to do properly

Sure, but I think the RFC needs to make a sufficiently clear argument that this is not "wrong most of the time". At least in the way I have encountered newtypes, I think it is wrong most of the time. Most of the time, in my experience, one specifically does not want implicit conversions from the inner type to the newtype. As-is, the RFC doesn't even acknowledge that this might ever be wrong, which is concerning.

The one exception I can think of is when the newtype is solely added to overcome the orphan rule, but one doesn't actually care about introducing a separate concept. But that usecase is not even mentioned in the RFC. Instead, the RFC mentions "introducing a separate concept" and then goes on to describe an API design that makes is super easy to accidentally mix up the new concept with its field type.

@Kobzol
Copy link
Member Author

Kobzol commented Jun 12, 2025

Depends on the use-case, I guess, most of the newtypes I create implement From, and they can hold all values of the inner type.

As a more general point, this "just" makes it easier to implement From. It can be used on anything, not just the newtype pattern specifically, and we can't ofc guarantee that the user does the right thing. It's just a syntactical shortcut to generate a boilerplate implementation of From (and I hope in the future also AsRef, Deref, Iterator, etc., with additional RFCs, of course).

@RalfJung
Copy link
Member

As a more general point, this "just" makes it easier to implement From. It can be used on anything, not just the newtype pattern specifically, and we can't ofc guarantee that the user does the right thing. It's just a syntactical shortcut to generate a boilerplate implementation of From

Yeah, I get that. But I find it concerning that the one concrete motivation given in the RFC is a big red flag IMO -- I'd not accept such code in a codebase I maintain, since I consider it as subverting the newtype benefits.

I can't think of any From instance I ever wrote that would be aided by this derive (unlike for many of the other traits you mention). Maybe I am using the language in a weird way, it's obviously quite hard to say whether your patterns or mine are more representative.

@RalfJung
Copy link
Member

To be clear, I'm not very opposed to the feature, I just think the RFC doesn't do a good job arguing for it, and it fails to acknowledge the downsides.

@Kobzol
Copy link
Member Author

Kobzol commented Jun 12, 2025

I even included some statistics in the RFC on how common this is, so that shows that it is being done in the ecosystem. But I don't think that matters very much for this RFC, tbh. The true downsides of this feature could be things like forward (in)compatibility or interaction with other language features.

The fact whether actually implementing From for a given type is a good or a bad idea shouldn't be decided by the compiler, that's up to the user. All this RFC proposes is to make it easier to implement From, that's it. I included the newtype to sufficiently motivate the use-case of "there are situations where you have exactly one field and From is useful". Since I implement From for most of my newtypes, and found many usages of this in the ecosystem, I considered it to be a valid use-case, but I don't think it's load-bearing for this RFC. Note that it is named "enable derive(From) for single-field structs", not "enable derive(From) for newtypes".

@Kobzol
Copy link
Member Author

Kobzol commented Jun 12, 2025

Removed the motivation for V: Into<Newtype> for tests (I agree that it seems "too dirty" without seeing the context where it does help to remove boilerplate), and reworded a paragraph about when it makes or doesn't make sense to implement From.

@RalfJung
Copy link
Member

Thanks, that helps. :)

@BurntSushi
Copy link
Member

My main concern here is that this is not quite as broadly applicable as it seems, since deriving From strongly suggests that there are no additional invariants enforced by the newtype. (I think this echos a similar or identical concern as @RalfJung.) I find I am mostly convinced by @scottmcm's thoughts on that though. While I sometimes can benefit from a derive like this, a quick look at my other From impls suggests it's pretty rare overall relative to the number of From impls in total.

But I think this is nice for reducing boiler plate when folks want a quick newtype that isn't enforcing additional invariants. (And is perhaps being used as a marker or as a different target for other traits.)

I also think this can just be a libs-api FCP.

@scottmcm
Copy link
Member

(quoting @BurntSushi above)

I find I am mostly convinced by #3809 (comment).

TBH, while I don't disagree with what I wrote earlier, the arguments about which direction of from is more useful does seem relevant to me.

If I have

#[derive(From)]
pub struct Even(u32);

Then the impl From<u32> for Even { … } is just wrong, but the impl From<Even> for u32 { … } would be correct and useful! The constructor-from is relatively rare and would often be wrong, where as the destructor-from is appropriate unless the newtype really needs to hide the internals. (But even if you're making a box-like type where From<MyBox> for *const () is probably a bad idea, it's probably at least not unsound the way that From<*const ()> for MyBox would be.)

Thus it does feel odd to me that we'd add this RFC's less-applicable case without having something for the more-applicable case (aka the derive(Into) or something). And if this isn't that common, it makes me wonder if it should be derive(FromInner) or something to allow potentially other meanings of derive(From) in future that would be more commonly applicable.

I also think this can just be a libs-api FCP.

I would be perfectly happy for libs-api to take over the FCP and thus I not have to decide one way or the other 🙃

@Kobzol
Copy link
Member Author

Kobzol commented Jun 27, 2025

The constructor-from is relatively rare

I think that depends a lot on your use-cases. In terms of the code I write, I use newtypes without additional invariants for something like 90% of use-cases. PullRequestId, UserId, TaskId, WorkerId, I almost always just need literally a new type, without any invariants. That's the case where this derive helps the most.

I agree that the other direction is also very useful, but I don't think we need to resolve that in this RFC, unless you can imagine a situation where #[derive(From)] would actually generate the other direction. Do you think that's feasible? (I don't, as it would behave in a completely way than all other built-in derives).

@scottmcm
Copy link
Member

scottmcm commented Jun 27, 2025

I guess looking at #3809 (comment) all of libs-api has checked their boxes, so I'll just abstain to allow their decision to land, and if they end up concerning anything because of discussion, that's up to them.

@rfcbot abstain

@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Jun 27, 2025
@rfcbot
Copy link
Collaborator

rfcbot commented Jun 27, 2025

🔔 This is now entering its final comment period, as per the review above. 🔔

@tmandry
Copy link
Member

tmandry commented Jun 28, 2025

Meh, I share the concerns raised by @RalfJung, @BurntSushi, and @scottmcm.

On one hand I want "obvious" things like this to make newtypes better. On the other, I'm not sure it meets that bar because many newtypes don't actually want this, and it takes up a big slice of linguistic space we could potentially redistribute to other meanings. Sometimes you want the inverse From impl (and only that one), sometimes you want both; sometimes you want Into because of orphan rules, etc.

Personally I'd rather see a slightly more "maximalist vision" that this fits into as a first step. That could take the form of a more expansive future possibilities section that covers the situations I listed above (or reasons why we shouldn't cover them).

EDIT: I could be convinced there are a bunch of newtypes out there where this derive is idiomatic and a good tradeoff. In my code it usually is not. One point I'll make in favor of the RFC is that this behavior seems like the most obvious one by far for #[derive(From)] with nothing else.

@tmccombs
Copy link

I agree that it would be nice to have the other direction as well, but I would find it very surprising if derive(From) created an impl for a struct other than the on it is an attribute for. derive(Into) is a little bit better, but it still seems surprising that it doesn't add an impl of Into, and then it prevents deriving tne Into trait itself in situations where that is necessary. Maybe, we could use derive Into, with an additional attribute to indicate Into is implemented indirectly by implementing From on the inner type. But then the more common case is more verbose, which doesn't seem great.

I wonder if instead of using the existing derive attribute, it would make more sense to create a new attribute for a generalization of this, where if you have a generic trait with a single type paramater, like A<T>, then something like #derive_on(C, A) on a type B could mean "derive an impl A<B> for C". Name subject to bikeshedding of course. Although that adds complexity, and would probably mean adding support for a different kind of derive macro (or maybe just pass some additional metadata to existing derive macros?).

Alternatively, it could use a derive macro with a new name that doesn't correspond to an existing trait.

@Kobzol
Copy link
Member Author

Kobzol commented Jun 28, 2025

Meh, I share the concerns raised by @RalfJung, @BurntSushi, and @scottmcm.

On one hand I want "obvious" things like this to make newtypes better. On the other, I'm not sure it meets that bar because many newtypes don't actually want this, and it takes up a big slice of linguistic space we could potentially redistribute to other meanings. Sometimes you want the inverse From impl (and only that one), sometimes you want both; sometimes you want Into because of orphan rules, etc.

Personally I'd rather see a slightly more "maximalist vision" that this fits into as a first step. That could take the form of a more expansive future possibilities section that covers the situations I listed above (or reasons why we shouldn't cover them).

EDIT: I could be convinced there are a bunch of newtypes out there where this derive is idiomatic and a good tradeoff. In my code it usually is not. One point I'll make in favor of the RFC is that this behavior seems like the most obvious one by far for #[derive(From)] with nothing else.

I think this RFC got bogged down with newtypes too much. It's just one use-case, but this derive is a general tool, used to reduce boilerplate for trivial From impls. Nothing less, nothing more. Some other derives can also be semantically wrong, but that's not the compiler's/language's concern (IMO).

I understand the desire to also explore the other direction of the impl, but I'd like to keep the RFC minimal, as I think that it stands on its own and it is useful even without the other direction (otherwise I wouldn't propose it). The only reason why it should be necessary to decide what to do about the other direction in this RFC would IMO be if the proposed derive was not forward compatible. Do you see that as being the case? I think that based on the current derives, we can't "afford" for derive(From) to do anything else than what I proposed here. We could still add derive(Into), derive(From(inner)) or something like that later, I just don't see how this proposal could not be forward compatible with these changes.

@RalfJung
Copy link
Member

Single-field types are newtypes, so I don't see how this derive can be used for anything else?

@Kobzol
Copy link
Member Author

Kobzol commented Jun 28, 2025

I wouldn't classify it in that way. The newtype pattern is a (design) pattern; these always describe some motivation for why they are being used. For the newtype pattern, the motivation is to introduce a new element in the type system that is backed by an existing data type, to distinguish them in the type system even if they have the same in-memory representation. Potentially you can also add additional invariants to the inner data type (I'd argue these are two different patterns with different use-cases, which is also a part of the opposing viewpoints in this RFC, where some people use newtypes for the invariants, but others use it only for adding a new type system element).

But you can have code that looks in that exact same way (single-field struct), but it does not uphold the pattern, because it has a different motivation. This is also known from the classical GoF patterns, where multiple patterns can have exactly the same code signature, but different motivation for why they are being used. Sometimes a single-field struct is just a single-field struct, and you are not creating it for the reason of introducing a new element in the type system.

For example, recently I had a use-case where I had a struct with a lot of fields, and I wanted to have the option to use the struct literal constructor to initialize these fields (to avoid a new function that has 15 parameters), but I also didn't want these fields to be pub, because I needed them to be read-only after initialization. I solved it by wrapping that struct in another tuple struct, like this:

struct Config { paramA, paramB, paramC }

struct ReadOnlyConfig(Config);

let config = ReadOnlyConfig::from(Config { paramA: 1, paramB: 2, paramC: 3 }));

The motivation here was not to distinguish these two types in the type system, nor to add any invariants. I just wanted to combine struct literal initialization with private access to the fields.

(btw: #[derive(From)] would be quite useful here :) )

@RalfJung
Copy link
Member

I would argue in that case you want to distinguish Config and ReadOnlyConfig in the type system, so I'd still call this a newtype. 🤷


But anyway, the real question is whether the way you use newtypes is common enough to justify this extension. The way I use the language, I don't think I'd use this feature more than once in a blue moon, and others have raised similar concerns.

In the RFC, you give evidence for this:

  • There is this code search. A significant fraction of the results are for enums or multi-field structs though, so those have to be discounted. (I have no idea what this even does on a multi-field struct, but that does seem to be a thing.) But, most of the pages in the search result seem to have at least one single-field struct in it, so that's still on the order of 1k uses in the ecosystem.
  • And then there's "In the analyzed 168 crates, 559 single-field tuple structs were found, and 49 out of them contained the From implementation from their field type." 49 occurrences in 168 crates does not strike me as a evidence for this being super common (the vast majority of single-field structs do not have a From). But, it's also not nothing.
  • It does turn out we use newtypes like this in rustc, at least some of the time.

I don't have a good sense for how much potential use is "enough" to justify this as a core language feature. I admit this is already a lot more than I expected.


One point I'll make in favor of the RFC is that this behavior seems like the most obvious one by far for #[derive(From)] with nothing else.

Yeah, that's a good point. If we do eventually want to have a derive for the other direction (which I'd find a lot more useful), it would probably be quite confusing to call that derive(From)...

@Kobzol
Copy link
Member Author

Kobzol commented Jun 28, 2025

to justify this as a core language feature

This is overselling it a bit, I think. Even t-lang said that they don't consider this to be a big lang deal, and it's mostly "just" a t-libs-api concern. I view this as filling a gap in the combination of two existing features (derive and built-in traits), rather than introducing a new feature.

Anyway, the FCP has started, if someone wants to register formal complain, there's still time :)

@RalfJung
Copy link
Member

RalfJung commented Jun 28, 2025

Yeah fair, a core library (as in, libcore) feature would have been more accurate. :)

@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. and removed I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-2 Lang team prioritization drag level 2. labels Jul 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. T-lang Relevant to the language team, which will review and decide on the RFC. T-libs-api Relevant to the library API team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.