Skip to content
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

Introduce a newtype keyword. #186

Closed
wants to merge 7 commits into from
Closed

Introduce a newtype keyword. #186

wants to merge 7 commits into from

Conversation

treeman
Copy link

@treeman treeman commented Jul 26, 2014

@laszlokorte
Copy link

I am splitting hairs but there is a semantic error in your examples: Multiplying two "inchy" numbers does not give you another inch but a square-inch.

I like the overall idea but I think to evaluate to real usefulness there should be a better example.
Your example is more of a counter example that shows that you do NOT get real type safety but cloning a type into a new name.

multiplying: int, int -> int
multiplying: cm, cm -> cm^2

An error that could still happen with your proposal is:

let x: cm = 10
let y: cm = 5
let area: cm = area(x,y)
let foo: cm = area
//...
let otherArea: cm = area(x, foo) // semantically not quite correct

@treeman
Copy link
Author

treeman commented Jul 26, 2014

Ah dang you are of course right! Not sure what I was thinking.

@treeman
Copy link
Author

treeman commented Jul 26, 2014

There. Hopefully it's a bit more clear.

let dist = Inch(calc_distance(start_inch as int, end_inch as int));
```

It also looses type safety.
Copy link

Choose a reason for hiding this comment

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

loses or loosens

@pnkfelix
Copy link
Member

This seems like it could be a macro that expands into the struct based new-type and a collection of impl's that delegate...

@pczarn
Copy link

pczarn commented Jul 26, 2014

Exactly, just like bitflags that are created with a macro. This idea is useful for ergonomics only.

The newtype keyword introduces a function, too. In the example, it's Inch(uint) -> Inch.

Is the constructor accessible from other modules? As in struct Inch(pub uint);

@glaebhoerl
Copy link
Contributor

Introduce a new keyword: newtype. It introduces a new type, with the same capabilities as the underlying type, but keeping the types separate.

Could you go into more precise detail about what this means? What does "same capabilities" imply? What's happening under the hood? On what basis is the second, modified example code accepted?

If it boils down to just the fact that, given newtype Bar = Foo, Bar inherits all of the trait impls defined for Foo, then I think a more appropriate remedy would be to add a feature modelled after GHC's GeneralizedNewtypeDeriving. E.g.:

#[deriving(Sub)]
struct Inch(uint);

#[deriving(Sub)]
struct Cm(uint);

would be possible. This would require being more explicit about which "capabilities" the new type should inherit, but it's not clear that this would entirely be a drawback.

@treeman
Copy link
Author

treeman commented Jul 26, 2014

Hmm I had not considered using a macro for this. If it works then that could be preferable.

I am a bit curious on how newtype on struct's with defined trait impl would work with the macro approach? I'm not comfortable enough with rust to see exactly how that would work using delegates.

If we only would allow newtype on primitives, then I agree a macro approach would work fine. But allowing generics and struct definitions, newtype will be more expressive and powerful.

Is the constructor accessible from other modules? As in struct Inch(pub uint);

Following the scoping of other types, pub newtype Inch = uint would be accessible from other modules, but newtype Inch = uint would not. Naturally it should be able to use them with use module::my_type and expose them with pub use module::my_type.

The newtype keyword introduces a function, too. In the example, it's Inch(uint) -> Inch.

That's a mistake, thanks for pointing it out. It should be initialized directly from the number literal.

Could you go into more precise detail about what this means? What does "same capabilities" imply? What's happening under the hood? On what basis is the second, modified example code accepted?

Yes I need to that, thanks.

If it boils down to just the fact that, given newtype Bar = Foo, Bar inherits all of the trait impls defined for Foo

You are correct. Bar would inherit all of Foo's trait impls.

Now this raises some interesting questions. Would we be allowed to define our own base impl for Foo? Would we be able define new trait impls? The answer could depend on implementation details but I am inclined to say no.

then I think a more appropriate remedy would be to add a feature modelled after GHC's GeneralizedNewtypeDeriving. E.g.:

#[deriving(Sub)]
struct Inch(uint);

#[deriving(Sub)]
struct Cm(uint);

would be possible. This would require being more explicit about which "capabilities" the new type should inherit, but it's not clear that this would entirely be a drawback.

It's an interesting idea. As you say it might not necessarily be a drawback, in some cases it might also be an advantage. I don't know how one would implement the deriving directives, but it sure could be possible.

@dobkeratops
Copy link

imagine if tuple structs (and plain tuples) could be made to delegate methods and fields to all their components (prioritised by position), then you could use them more effectively as new types, and as intersection-types .. it could be a superior replacement for some of the uses of struct-inheritance in c++. this would make it easy to refactor code between OOP and component styles, and refactor code whilst reducing dependancies in a system

@pnkfelix
Copy link
Member

@treeman

Here is your code ported to the sort of macro I am thinking of: live on the playpen

@jfager
Copy link

jfager commented Jul 27, 2014

@pnkfelix That's cool, but it's more like newtype_uint than a general newtype. How would you write a macro that impls the correct traits from the base type, for an arbitrary base type?

@pnkfelix
Copy link
Member

@jfager Well, personally I think the set of "correct traits" is impossible to automatically infer ... e.g. like the comment above says, should you get multiplication for inches? It doesn't really make sense from the point-of-view of what they represent.

So really I think one should be providing a list of the primitve operator traits to implement. I did not include that in my macro definition, but I think it is feasible to revise it to do so.

It would probably be better to use a deriving form for this, where one writes out the set of automatically implemented traits explicitly, as suggested in @glaebhoerl 's comment above. This would let the abstraction author pick and choose which traits make sense to use the automagically generated impls.

(Of course its possible that the above is not what the author is asking for -- that they really do want to get every trait, including those like multiplication that do not make sense to me.)

@reem
Copy link

reem commented Jul 28, 2014

The most valuable part of this proposal is the potential to add something akin to GeneralizedNewtypeDeriving from GHC.

The current overhead in creating tons of impls for wrappers around common types discourages users from creating newtypes to enforce compile-time guarantees and instead encourages them to just go ahead and use the plain type.

For instance, if, as a user, I have to write:

struct Inch(uint);

impl Add<Inch, Inch> {
  // etc.
}

and write boilerplate impls for every single trait I want from the underlying type, I might just not use them. If I could instead do:

#![feature(generalized_newtype_deriving)] // or whatever

#[deriving(Add, Sub, etc.)] // Compiler error if uint does not impl any of these traits and they can't be derived normally.
newtype Inch(uint);

I would be significantly more likely to use newtypes to make compile time guarantees. Boilerplate trait impls are one of rust's ergonomic weak points in my opinion, and features like this go a long way to reduce the amount of boilerplate one has to write.

@alexchandel
Copy link

So the "newtype" inherits all of the base type's fields and methods, yet is a more specialized form of the parent. I assume it can also be passed to a function taking the parent type. This is basically limited struct inheritance, right? The only limitation is that the derived type is prevented from adding new fields.

@treeman
Copy link
Author

treeman commented Jul 28, 2014

Thanks @pnkfelix for the macro implementation.

Unfortunately it doesn't seem to handle arbitrary traits, and I don't know how we would accomplish that.

As it seems now, the better approach would indeed be like what @reem and @glaebhoerl suggests. Would it be possible to allow us to derive from arbitrary types and traits?

struct Parent { ... }

trait CustomTrait {
    fn do_struf() { ... }
}

impl CustomTrait for Parnet { ... }

#[deriving(CustomTrait)]
struct NewType(Parent);

Also could this be applicable for all tuple structs?

#[deriving(Eq, Sub)]
struct Vector(int, int);

let a = Vector(2, 3);
let b = Vector(4, 2);
let ab = b - a;
assert!(ab == Vector(2, -1))

And how to handle the methods of a struct? Should we be allowed explicitly derive them or might it be implicit?

impl Parent {
    fn fun() { ... }
}

#[deriving(self)] // or something?
struct NewType(Parent);

let x: NewType = ...;
x.fun(); // allowing this

So the "newtype" inherits all of the base type's fields and methods, yet is a more specialized form of the parent. I assume it can also be passed to a function taking the parent type. This is basically limited struct inheritance, right? The only limitation is that the derived type is prevented from adding new fields.

The idea was not struct inheritance as it would not be possible for a newtype to be passed to a function taking the parent type, it would have to take traits instead.

@treeman
Copy link
Author

treeman commented Jul 28, 2014

And I also agree with @pnkfelix and @laszlokorte that the current proposal isn't semantically sound, as multiplication doesn't make sense for Inch. Explicitly specifying what to derive seems to avoid that issue.

@glaebhoerl
Copy link
Contributor

Just based on what GHC's GNTD does:

Would it be possible to allow us to derive from arbitrary types and traits?

Yes.

Also could this be applicable for all tuple structs?

What would the "base" type be in this example? The tuple type (int, int)? If so... maybe. If you were to write it as struct Vector((int, int)) instead, then more obviously "yes". I'm not sure that it would be a good idea to treat the two as equivalent.

And how to handle the methods of a struct? Should we be allowed explicitly derive them?

No. In this case you would have to explicitly wrap/unwrap the newtype. (Again, if we were to do the same things GHC does, which has been proven to work well. We would have to think much harder about doing them differently.)

Mention GNTD as a useful, possibly preferred, alternative.
@treeman
Copy link
Author

treeman commented Jul 28, 2014

That's cool.

What would the "base" type be in this example? The tuple type (int, int)? If so... maybe. If you were to write it as struct Vector((int, int)) instead, then more obviously "yes". I'm not sure that it would be a good idea to treat the two as equivalent.

The written out type should be simply Vector, or perhaps equivalently, struct Vector((int, int)). It would be inconvenient to type it out all the time though. But perhaps that is what you mean with "base" type?

No. In this case you would have to explicitly wrap/unwrap the newtype. (Again, if we were to do the same things GHC does, which has been proven to work well. We would have to think much harder about doing them differently.)

That's fine. It could still be a way for code reuse:

struct Base<T> {
    x: T,
    y: T
}
impl<T> Eq for Base<T> { ... }

#[deriving(Eq)]
struct Vector(Base<int>);

#[deriving(Eq)]
struct Point(Base<int>);

assert!(Vector(1, 2) == Vector(1, 2));
assert!(Point(2, 3) != Point(1, 2));
assert!(Vector(1, 2) == Point(1, 2); // Type error

This way we could also choose to implement Sub for Vector but not for Point. And of course separate methods.

@pczarn
Copy link

pczarn commented Jul 28, 2014

Would it be possible to allow us to derive from arbitrary types and traits?

Yes, after changes to deriving. It currently assumes that std is the standard library with all derived traits. Maybe that invocation would be #[deriving(self::CustomTrait)].

Also could this be applicable for all tuple structs?

Yes. Similarly, all fields are cloned separately in a struct that derives Clone.

And how to handle the methods of a struct? Should we be allowed explicitly derive them?

I don't think so. We couldn't derive the methods of struct Vector(T, U).

@glaebhoerl
Copy link
Contributor

What would the "base" type be in this example? The tuple type (int, int)? If so... maybe. If you were to write it as struct Vector((int, int)) instead, then more obviously "yes". I'm not sure that it would be a good idea to treat the two as equivalent.

The written out type should be simply Vector, or perhaps equivalently, struct Vector((int, int)). It would be inconvenient to type it out all the time though. But perhaps that is what you mean with "base" type?

The "base" type is the type which the newtype is a newtype "of" (e.g., in your proposal, newtype new = base). The way GNTD works is that you can consider a trait as declaring a struct:

trait Eq {
    fn eq(&self, other: &Self) -> bool;
    fn neq(&self, other: &Self) -> bool;
}

// corresponds to:
struct Eq<Self> {
    eq: fn(&Self, &Self) -> bool,
    neq: fn(&Self, &Self) -> bool
}

and impls of it as declaring instances of the struct:

impl Eq for Foo {
    fn eq(&self, other: &Foo) -> bool { ...def1... }
    fn neq(&self, other: &Foo) -> bool { ...def2... }
}

// corresponds to:
static IMPL_EQ_FOO: Eq<Foo> = Eq {
    eq: ...def1..., // do we have `fn` literals?
    neq: ...def2...
};

And given:

#[deriving(Eq)]
struct Foo(Bar);

what GNTD does is simply transmute the Eq impl for Bar, of type Eq<Bar>, to Eq<Foo>, and uses it as the impl for Foo. (Or more precisely, it transmutes each of the methods separately, but that's unnecessary detail in this context.)

(For the record, #[deriving(Eq)] here is hypothetical syntax, and not necessarily the one we'd actually want to use.)

@huonw
Copy link
Member

huonw commented Jul 28, 2014

From an implementation perspective, the current macro-based #[deriving] cannot work for implementing arbitrary traits. It currently just creates the AST structures for the requested implementations, including writing out the required types, generics (rust-lang/rust#7671), method signatures and method implementations (and these have to fit the trait/types perfectly), and this well before any detailed knowledge is available.

Specifically, there's no way to work out what methods/signatures should be implemented for a generalised #[deriving(Foo)] (or even how many there are) because no knowledge is known about the trait Foo... it's not even known if the name Foo refers to a trait.

That is, GNTD would be a non-trivial extension to our current deriving infrastructure, moving it from being a plain macro to something with (relatively) deep compiler hooks.

(As I was typing this, @glaebhoerl wrote a description of a possible implementation-via-compiler-magic.)

@alexcrichton
Copy link
Member

cc rust-lang/rust#8353

@Ericson2314
Copy link
Contributor

I agree tuple struct + newtype deriving seems more orthogonal. One thing I've never understood why GHC didn't provide is:

#[deriving_all_but(...)]
struct NewType(Type)

Might as well have both, but IMO this variant is the more useful one.

@ftxqxd
Copy link
Contributor

ftxqxd commented Aug 1, 2014

👍 This is a much-needed improvement—AIUI Go even went so far as to make all type declarations create new types.

Can newtypes be casted to their base types? For example:

newtype Metre = uint;

fn frobnicate(x: uint) -> uint { x * 2 + 14 - 3 * x * x }

println!("{}", frobnicate(x as uint));

I like the idea of adding something like GeneralizedNewtypeDeriving to Rust.

Also, some of the semantics aren’t so well-defined. uint implements Add<uint, uint>, so surely a newtype Inch = uint would also implement Add<uint, uint>, not Add<Inch, Inch>? In the RFC it’s sort of implied that all generic parameters in the trait matching the base type are converted to the new type, but it’s not explicitly stated AFAICT. This could also be very confusing—uint implements Shl<uint, uint>, so presumably newtype Inch = uint would implement Shl<Inch, Inch>. But int implements Shl<uint, int>, so newtype Inch = int would implement Shl<uint, Inch>. That is certainly counter-intuitive—one would expect newtype Inch = uint to implement Shl<uint, Inch>, because all integrals implement Shl<uint, Self>.

The best way to solve this IMO is to allow Self in trait implementations. This would require changing things like impl Shl<uint, uint> for uint to impl Shl<uint, Self> for uint. Then, a newtype declaration would just replace the Selfs with the newtype being defined.

@nrc
Copy link
Member

nrc commented Aug 7, 2014

+1 The fact that there exists a well known hack for implementing this already shows there is a need and makes me want a real solution so we can avoid the hack.

Should explicit coercions (using as) be allowed between newtypes and the old type? In both directions? (I think yes, but I haven't thought about it in depth).

@reem
Copy link

reem commented Aug 7, 2014

There should be explicit, but certainly not implicit, coercions. Since the newtype is always the same in memory as the type it is wrapping, I see no reason not to allow explicit casting - all it will do is remove many unnecessary destructurings.

@glaebhoerl
Copy link
Contributor

@nick29581

+1 The fact that there exists a well known hack for implementing this already shows there is a need and makes me want a real solution so we can avoid the hack.

I think the right "real solution" would be either GeneralizedNewtypeDeriving or module-scoped existentials a la ML. Do you have a reason to think otherwise?

@nrc
Copy link
Member

nrc commented Aug 7, 2014

They seem more complex solutions - why bother with the extra wrapping entailed by using a struct rather than a newtype? module scoped existentials seem strictly more complex and I don't see the motivation for the extra complexity.

@glaebhoerl
Copy link
Contributor

Sorry, I think I might've accidentally been conflating aspects of this discussion with another similar, recent one - to clarify, what were you referring to as the "well known hack" already in use?

@treeman
Copy link
Author

treeman commented Aug 13, 2014

Thanks @glaebhoerl for the explanation of GNTD.

Can newtypes be casted to their base types?

Should explicit coercions (using as) be allowed between newtypes and the old type? In both directions? (I think yes, but I haven't thought about it in depth).

Yes I don't see any reason why not.

@nick29581

They seem more complex solutions - why bother with the extra wrapping entailed by using a struct rather than a newtype? module scoped existentials seem strictly more complex and I don't see the motivation for the extra complexity.

Syntactically a newtype keyword looks and feels better to me, but I could also live with a struct wrapping. Generalizing over larger tuple structs might also be a powerful construct (as long as all types in the tuples implement the desired traits).

But the big advantage with GND is to exclude (or include) selected traits. When for example Mul doesn't make sense but Add and Sub does. Or maybe we don't want to support that in the language?

Could we combine the newtype with GND? By default we derive everything, but we can explicitly specify what to derive?

newtype DeriveAll = uint;

#[deriving(Add, Sub)]
newtype Inch = uint;

#[deriving_all_but(Mul, Div)]
newtype Cm = uint;

And what would it take to automatically discover all visible traits for a type?

@treeman
Copy link
Author

treeman commented Aug 13, 2014

@P1start

Also, some of the semantics aren’t so well-defined. uint implements Add<uint, uint>, so surely a newtype Inch = uint would also implement Add<uint, uint>, not Add<Inch, Inch>? In the RFC it’s sort of implied that all generic parameters in the trait matching the base type are converted to the new type, but it’s not explicitly stated AFAICT. This could also be very confusing—uint implements Shl<uint, uint>, so presumably newtype Inch = uint would implement Shl<Inch, Inch>. But int implements Shl<uint, int>, so newtype Inch = int would implement Shl<uint, Inch>. That is certainly counter-intuitive—one would expect newtype Inch = uint to implement Shl<uint, Inch>, because all integrals implement Shl<uint, Self>.

The best way to solve this IMO is to allow Self in trait implementations. This would require changing things like impl Shl<uint, uint> for uint to impl Shl<uint, Self> for uint. Then, a newtype declaration would just replace the Selfs with the newtype being defined.

Good points.

@nrc
Copy link
Member

nrc commented Sep 22, 2014

I would like to discuss this RFC at the weekly meeting this week (Tuesday). I will propose we close it and tag the RFC as postponed. I strongly believe we should have newtypes and generalised newtype deriving in Rust, however, I believe the work and discussion on the design should be postponed until after 1.0.

In order to ensure we can implement this later, I would like to reserve the newtype keyword. I would also like to discuss whether having keywords type and newtype is the best solution, so that we do reserve the right keywords.

If anyone has a strong argument for why this should be done before 1.0, or has an opinion on naming, please let me know.

@nrc nrc added the postponed RFCs that have been postponed and may be revisited at a later time. label Sep 23, 2014
@nrc
Copy link
Member

nrc commented Sep 23, 2014

Discussed at the weekly meeting today (https://github.com/rust-lang/meeting-minutes/blob/master/weekly-meetings/2014-09-23.md). This is definitely something we will consider in the future, but not before 1.0. We decided not to reserve a keyword - no one really liked newtype, but there were no better suggestions either. We hope to do this without a keyword (by macro, or using type with some decoration), otherwise we can use language versioning to introduce a keyword.

@nrc nrc closed this Sep 23, 2014
@pnkfelix pnkfelix mentioned this pull request Dec 9, 2014
@oli-obk oli-obk mentioned this pull request Dec 11, 2017
@Centril Centril added A-data-types RFCs about data-types A-syntax Syntax related proposals & ideas A-keyword Proposals relating to keywords. labels Nov 26, 2018
wycats pushed a commit to wycats/rust-rfcs that referenced this pull request Mar 5, 2019
…nique-history-location-state

RFC: Track unique history location state
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-data-types RFCs about data-types A-keyword Proposals relating to keywords. A-syntax Syntax related proposals & ideas postponed RFCs that have been postponed and may be revisited at a later time.
Projects
None yet
Development

Successfully merging this pull request may close these issues.