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
base: master
from

Conversation

Projects
None yet
@treeman
Copy link

treeman commented Jul 26, 2014

@laszlokorte

This comment has been minimized.

Copy link

laszlokorte commented Jul 26, 2014

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

This comment has been minimized.

Copy link
Author

treeman commented Jul 26, 2014

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

@treeman

This comment has been minimized.

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.

This comment has been minimized.

@pczarn

pczarn Jul 26, 2014

loses or loosens

@pnkfelix

This comment has been minimized.

Copy link
Member

pnkfelix commented Jul 26, 2014

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jul 26, 2014

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

dobkeratops commented Jul 26, 2014

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

This comment has been minimized.

Copy link
Member

pnkfelix commented Jul 27, 2014

@treeman

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

@jfager

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

pnkfelix commented Jul 27, 2014

@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

This comment has been minimized.

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

This comment has been minimized.

Copy link

alexchandel commented Jul 28, 2014

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jul 28, 2014

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

Clarify that newtype would not implement inheritance.
Mention GNTD as a useful, possibly preferred, alternative.
@treeman

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jul 28, 2014

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

alexcrichton commented Jul 28, 2014

@Ericson2314

This comment has been minimized.

Copy link
Contributor

Ericson2314 commented Jul 31, 2014

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.

@P1start

This comment has been minimized.

Copy link
Contributor

P1start 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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Aug 7, 2014

@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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Aug 7, 2014

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

This comment has been minimized.

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

This comment has been minimized.

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.

@alexcrichton alexcrichton force-pushed the rust-lang:master branch from 6357402 to e0acdf4 Sep 11, 2014

@aturon aturon force-pushed the rust-lang:master branch from 4c0bebf to b1d1bfd Sep 16, 2014

@nrc nrc self-assigned this Sep 18, 2014

@nrc

This comment has been minimized.

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 label Sep 23, 2014

@nrc

This comment has been minimized.

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 referenced this pull request Dec 9, 2014

Closed

Newtype deriving #508

@oli-obk oli-obk referenced this pull request Dec 11, 2017

Closed

Semantic newtypes #2242

wycats pushed a commit to wycats/rust-rfcs that referenced this pull request Mar 5, 2019

Merge pull request rust-lang#186 from bcardarella/features/bc/track-u…
…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
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.