Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upSemantic newtypes #2242
Conversation
treeman
and others
added some commits
Jul 26, 2014
This was referenced Dec 11, 2017
Centril
added
the
T-lang
label
Dec 11, 2017
Centril
reviewed
Dec 11, 2017
|
I like the moral behind this RFC but I'd like to discuss some details. |
| id type: | ||
|
|
||
| ```rust | ||
| type UserIndex is new usize; |
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
This syntax feels alien to me - why not simply:
newtype UserIndex(usize);or:
new type UserIndex(usize);Assuming we are allowed to introduce this to the grammar (with an epoch or whatever), to me this feels more direct - you know that it is a newtype from the fist syllable and it's more consistent with the current syntax rules. Perhaps you considered this syntax and rejected it - if so, why?
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
I assume there's a good reason we can't just retrofit any/all of the following (which are all semantically newtypes given #[repr(transparent)]):
struct New(Base);
struct New {
field_name: Base
}
enum New {
Variant(Base)
}
enum New {
Variant {
field_name: Base
}
}Does it have to do with coherence and/or backwards-compatibility? Still, I think it should be spelled out whatever the reason.
This comment has been minimized.
This comment has been minimized.
oli-obk
Dec 11, 2017
Contributor
newtype
was rejected in #186 (comment)
new type
I like it.
#[repr(transparent)]
that representation does not do any newtyping to the best of my knowledge. It just makes sure the memory representation in the backend is 100% the same as just the inner type
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
was rejected in #186 (comment)
2014 was a eons ago and before 1.0 ^,- I think there's a good argument from familiarity (Haskell) to be made for newtype as a syntax. Tho new type could be equally clear. There's the "is new a modifier on type, or is it a different concept altogether?"-discussion to be had.
that representation does not do any newtyping to the best of my knowledge. It just makes sure the memory representation in the backend is 100% the same as just the inner type
Would not new type New(Base); assume #[repr(transparent)] if transmute is to be a possibility? What I mean is that:
#[repr(transparent)]
struct New(Base);is semantically a new-type today - you just lack the auto-deriving capabilities. Those capabilities could perhaps be added for some traits (decidable by analysis on the syntax of the trait and not a specific list of traits?).
This comment has been minimized.
This comment has been minimized.
oli-obk
Dec 11, 2017
Contributor
Deriving creates more code for something that is just a semantic name change. Additionally it only works when you already know what traits to derive. Any traits implemented downstream for the base type won't get implemented for your type.
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
Of course, you have to explicitly say what types you want to derive - often, there might be many traits you do want to derive and perhaps 1-2 you want to leave out in order to give a different impl for the newtype. A common example of this scenario is the Monoid trait and numeric types.
| } | ||
| // Initialize the same way as the underlying types | ||
| let (start_inch, end_inch): (Inch, Inch) = (10, 18); |
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
What if Inch has a smart constructor explicitly rejecting subsets of usize? Seems like that use case isn't supported, which is what I expect from a feature called "newtype" (as used in Haskell).
This comment has been minimized.
This comment has been minimized.
| good(a); // Ok, Foo implements Sub | ||
| ``` | ||
|
|
||
| ## Derived traits |
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
The #![derive_unfinished(..)] RFC has some notes here on how we can be more flexible wrt. deriving by extending the syntax of derive(_unfinished) itself. Feel free to steal any of those notes =) Tho I understand if you want to conservative initially.
I think generalized-newtype-deriving would be the major ergonomics-boost coming from this proposal.
| contextual keywords `is` and `new`. The reason for using `is new` instead of | ||
| another sigil is that `type X = Y;` would be very hard to distinguish from any | ||
| alternative like `type X <- Y;` or just `type X is Y;`. | ||
|
|
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
Not a fan of this grammar, see newtype as an alternative syntax above.
| Newtypes can explicitly be converted to their base types, and vice versa. | ||
| Implicit conversions are not allowed. | ||
| This is achieved via the `Into` trait, since newtypes automatically implement | ||
| `From<BaseType>` and `From<NewType> for BaseType`. In order to not expose new |
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
This is achieved via the
Intotrait, since newtypes automatically implementFrom<BaseType>andFrom<NewType> for BaseType.
That sounds problematic. These impls would be available to anyone as soon as NewType is exported due to the way impls are not scoped in Rust. This means that you have no ability to write smart constructors that constrain BaseType further in the allowed set of values. While refinement types may partially (the expressiveness of refinement may/should not be turing complete, unlike unconstrained Rust-code, etc.) solve this in the future, we are far from having those currently. I think it is important that newtypes be allowed to provide only smart constructors such as NewType::new(..) -> Self that are user defined.
To that end, as an alternative I'd like to propose that the compiler generate a module:
mod NewType {
pub fn from_base(BaseType) -> NewType {..}
pub fn into_base(NewType) -> BaseType {..}Another alternative may be inherent and associated methods on NewType.
The From impls should only be generated and iff the user does derive(From, Into) or perhaps derive(Newtype) (with a possible actual trait Newtype) to avoid conflicts with custom-derive.
Some notes discussing T<NewType> -> T<BaseType> where T may be &, Box, Arc, Vec, .. , would be nice even if you are proposing we not allow those conversions. In that case, notes on why the conversions should not be allowed.
This comment has been minimized.
This comment has been minimized.
oli-obk
Dec 11, 2017
Contributor
This is an open question as stated at the end of the RFC ;)
I'm totally on board with scrapping these impls.
I personally don't need them. I'd be happy writing them out whenever needed.
Some notes discussing T -> T
I'll add those
| ## Implementation | ||
|
|
||
| The compiler would treat newtypes as a thin wrapper around the original type. | ||
| This means that just declaring a newtype does *not* generate any code, because |
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
This is inconsistent with:
This is achieved via the
Intotrait, since newtypes automatically implementFrom<BaseType>andFrom<NewType> for BaseType.
This comment has been minimized.
This comment has been minimized.
|
|
||
| We could save a keyword with this approach and we might consider a generalization | ||
| over all tuple structs. | ||
|
|
This comment has been minimized.
This comment has been minimized.
Centril
Dec 11, 2017
Contributor
Seems like an excellent idea! Please do consider that generalization =)
| complaints about it seem to suggest that something needs to be done. Even | ||
| the compiler itself uses generated newtypes extensively for special `Vec`s that | ||
| have a newtype index type instead of `usize`. | ||
|
|
This comment has been minimized.
This comment has been minimized.
kennytm
reviewed
Dec 11, 2017
|
Once upon a time, the D programming language had a I counter-propose we concentrate on unblocking RFC #1406 (delegation of implementation) and support user-defined literals instead. |
| println!("{}", frobnicate(x.into())); | ||
| let a: usize = 2; | ||
| let i: Inch = a; // Compile error, implicit conversion not allowed |
This comment has been minimized.
This comment has been minimized.
kennytm
Dec 11, 2017
Member
What is the difference between a and 2 that makes
let i: Inch = a; // illegal
let j: Inch = 12; // legal? Being a "literal"? What about a constant
const YARD: usize = 36;
let k: Inch = YARD;And expression
let day: Seconds = 24 * 60 * 60;What if the base type cannot be represented using any kind of literals
type RcInch is new Rc<usize>;
let ai: RcInch = Rc::new(0); // ?????
This comment has been minimized.
This comment has been minimized.
oli-obk
Dec 11, 2017
Contributor
The difference is that a has a type.
let a = 2;
let i: Inch = a; // works, because `a` is inferred to be `Inch`The same with expressions. It already works for choosing any of the base integer types. If inference is adjusted to newtypes, this will work out of the box just like the inferred variable type example above.
type RcInch is new Rc<usize>;
let ai: RcInch = Rc::new(0); // ERROR, expected RcInch, got Rc
let aj: RcInch = Rc::new(0).into(); // might work, but a lot of generics
let ak: RcInch = RcInch::new(0); // ok
This comment has been minimized.
This comment has been minimized.
kennytm
Dec 11, 2017
Member
@oli-obk Thanks for the clarifications.
In Rust only integer and floating point literals have unspecified types. I suppose we want to support these cases?
type Identifier<'a> is new &'a str;
type Verbose is new bool;
let default_verbose: Verbose = false;
let tag_name: Identifier<'static> = "center";Do we need to make these literals' types to be a generic {{boolean}} and {{string}}?
| In the derived trait implementations the basetype will be replaced by the newtype. | ||
|
|
||
| So for example as `usize` implements `Add<usize>`, `type Inch is new usize` | ||
| would implement `Add<Inch>`. |
This comment has been minimized.
This comment has been minimized.
kennytm
Dec 11, 2017
Member
This section requires a serious expansion. Without auto trait implementation, I don't see how this is better than simply introducing user-defined literals (1234_inch).
Examples to clarify:
usizehas an inherent methodfn trailing_zeros(self) -> u32, should it be available toInch?- If (1) is yes,
usizehas an inherent methodfn swap_bytes(self) -> usize, should it be available toInchand returns anInch? - If (2) is yes,
usizehas an inherent methodfn checked_neg(self) -> Option<usize>, should it be available toInchand returns anOption<Inch>? - If (2) is yes,
usizehas an associated (static) methodconst fn min_value() -> usize, should it be available toInch? usizeimplementsAdd<&usize>. WouldInchimplementAdd<&Inch>?- If (5) is yes,
&usizeimplementsAdd<&usize>, would&InchimplementAdd<&Inch>? - Should
usizeimplementAdd<Inch>?Add<&Inch>? usizeimplementsShr<T>for all integer typesT(u8,u32,usizeetc). ShouldInchimplementShr<usize>? ShouldInchimplementShr<Inch>?
This comment has been minimized.
This comment has been minimized.
oli-obk
Dec 11, 2017
Contributor
- yes, that's the entire point of semantic newtypes, you get everything the base type has
- yes
- yes
- yes
- yes
- yes
- no
- The strategy proposed in this RFC would result in
Inch: Shr<Inch>
I'll add these points in the RFC text
This comment has been minimized.
This comment has been minimized.
|
I've become convinced that a library based solution will be better. Reasons:
|
oli-obk
closed this
Dec 12, 2017
This comment has been minimized.
This comment has been minimized.
|
Something I don't think anyone pointed out in this thread is that |
oli-obk commentedDec 11, 2017
Introduce a newtype construction allowing newtypes to use the capabilities of the underlying type while keeping type safety.
TLDR:
type Foo = Bar;, but no implicit conversion betweenFooandBarRendered