From 28a7b73c728e9e8a73ad92aaf8057377da14675f Mon Sep 17 00:00:00 2001 From: Jonas Hietala Date: Sat, 26 Jul 2014 12:10:44 +0200 Subject: [PATCH 1/7] Introduce a newtype keyword. --- active/0000-newtype-keyword.md | 119 +++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 active/0000-newtype-keyword.md diff --git a/active/0000-newtype-keyword.md b/active/0000-newtype-keyword.md new file mode 100644 index 00000000000..1f944904f1d --- /dev/null +++ b/active/0000-newtype-keyword.md @@ -0,0 +1,119 @@ +- Start Date: 2014-07-26 +- RFC PR #: (leave this empty) +- Rust Issue #: (leave this empty) + + +# Summary + +Introduce a `newtype` construction allowing newtypes to use the +capabilities of the underlying type while keeping type safety. + + +# Motivation + +Consider the situation where we want to create separate primitive +types. For example we want to introduce an `Inch` and a `Cm`. These +could be modelled with `uint`, but we don't want to accidentally +mix the types. + +With the current newtypes: + +``` +// We want to do generic manipulations +fn calc_area>(w: T, h: T) -> T { w * h } + +struct Inch(uint); +struct Cm(uint); + +let inch = Inch(10); +let cm = Inch(3); + +// We must explicitly destruct to reach the values +let Inch(inch_val) = inch; +let Cm(cm_val) = vm; + +let inch_area = Inch(calc_area(inch, inch)); +let cm_area = Cm(calc_area(cm, cm)); // type Cm +println!("area: {} and {}", inch_area, cm_area); +``` + +This is verbose, but at least the types don't mix. +We could explicitly define traits for the types, but that's duplication +if want the same capabilities as the underlying type. + +Another option is to use the `type` keyword, but then we loose type safety: + +``` +type Inch = int; +type Cm = int; + +let inch: Inch = 10; +let cm: Cm = 2; + +let oops = inch + cm; // not safe! +``` + + +# Detailed design + +Introduce a new keyword: `newtype`. It introduces a new type, with the same +capabilities as the underlying type, but keeping the types separate. + +``` +fn calc_area>(w: T, h: T) -> T { w * h } + +newtype Inch = uint; +newtype Cm = uint; + +let inch = Inch(10); +let cm = Inch(3); + +let inch_area = calc_area(inch, inch); // type Inch +let cm_area = calc_area(cm, cm); // type Cm +println!("area: {} and {}", inch_area, cm_area); + +let not_allowed = inch + cm; // Compile error +``` + +The grammar rules will be the same as for `type`. +It would also allow generics, like `type`: + +``` +struct A { n: N, m: M } +newtype B = A; +``` + +Just like `type` does. + + +# Drawbacks + +It adds a new keyword to the language and increases the language complexity. + + +# Alternatives + +* Keep it the same + + It works, but life could be simpler. + +* `as` could be used to convert from a newtype to the underlying value: + + ``` + struct Inch(int); + + let v: Inch = Inch(10); + println!("inches: {}", v as int); + ``` + + But we still need to explicitly cast when using generic functions: + + ``` + let area = Inch(calc_area(v as int, v as int)); + ``` + + +# Unresolved questions + +None yet + From 33d26ce8f4e366b591d98e315bb8df8d07f56dfb Mon Sep 17 00:00:00 2001 From: Jonas Hietala Date: Sat, 26 Jul 2014 13:43:58 +0200 Subject: [PATCH 2/7] More logical example. --- active/0000-newtype-keyword.md | 56 +++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/active/0000-newtype-keyword.md b/active/0000-newtype-keyword.md index 1f944904f1d..55d3d269f4d 100644 --- a/active/0000-newtype-keyword.md +++ b/active/0000-newtype-keyword.md @@ -19,22 +19,29 @@ mix the types. With the current newtypes: ``` -// We want to do generic manipulations -fn calc_area>(w: T, h: T) -> T { w * h } - struct Inch(uint); struct Cm(uint); -let inch = Inch(10); -let cm = Inch(3); +// We want to do generic manipulations +fn calc_distance>(start: T, end: T) -> T { + end - start +} + +let (start_inch, end_inch) = (Inch(10), Inch(18)); +let (start_cm, end_cm) = (Cm(2), Cm(5)); // We must explicitly destruct to reach the values -let Inch(inch_val) = inch; -let Cm(cm_val) = vm; +let (Inch(start), Inch(end)) = (start_inch, end_inch); +let inch_dist = Inch(calc_distance(start, end)); + +let (Cm(start), Cm(end)) = (start_cm, end_cm); +let cm_dist = Cm(calc_distance(start, end)); + +let (Inch(inch_val), Cm(cm_val)) = (inch_dist, cm_dist); +println!("dist: {} and {}", inch_val, cm_val); -let inch_area = Inch(calc_area(inch, inch)); -let cm_area = Cm(calc_area(cm, cm)); // type Cm -println!("area: {} and {}", inch_area, cm_area); +// Disallowed compile time +let not_allowed = calc_distance(start_inch, end_cm); ``` This is verbose, but at least the types don't mix. @@ -44,8 +51,8 @@ if want the same capabilities as the underlying type. Another option is to use the `type` keyword, but then we loose type safety: ``` -type Inch = int; -type Cm = int; +type Inch = uint; +type Cm = uint; let inch: Inch = 10; let cm: Cm = 2; @@ -60,19 +67,24 @@ Introduce a new keyword: `newtype`. It introduces a new type, with the same capabilities as the underlying type, but keeping the types separate. ``` -fn calc_area>(w: T, h: T) -> T { w * h } - newtype Inch = uint; newtype Cm = uint; -let inch = Inch(10); -let cm = Inch(3); +// We want to do generic manipulations +fn calc_distance>(start: T, end: T) -> T { + end - start +} + +let (start_inch, end_inch) = (Inch(10), Inch(18)); +let (start_cm, end_cm) = (Cm(2), Cm(5)); -let inch_area = calc_area(inch, inch); // type Inch -let cm_area = calc_area(cm, cm); // type Cm -println!("area: {} and {}", inch_area, cm_area); +let inch_dist = calc_distance(start_inch, end_inch); +let cm_dist = calc_distance(start_cm, end_cm); -let not_allowed = inch + cm; // Compile error +println!("dist: {} and {}", inch_dist, cm_dist); + +// Disallowed compile time +let not_allowed = calc_distance(start_inch, end_cm); ``` The grammar rules will be the same as for `type`. @@ -109,9 +121,11 @@ It adds a new keyword to the language and increases the language complexity. But we still need to explicitly cast when using generic functions: ``` - let area = Inch(calc_area(v as int, v as int)); + let dist = Inch(calc_distance(start_inch as int, end_inch as int)); ``` + It also looses type safety. + # Unresolved questions From b3fe5e7567f17d752e55a0558b104cc932f1dd70 Mon Sep 17 00:00:00 2001 From: Jonas Hietala Date: Sat, 26 Jul 2014 14:12:30 +0200 Subject: [PATCH 3/7] Spelling mistake --- active/0000-newtype-keyword.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active/0000-newtype-keyword.md b/active/0000-newtype-keyword.md index 55d3d269f4d..d7c9dd7de78 100644 --- a/active/0000-newtype-keyword.md +++ b/active/0000-newtype-keyword.md @@ -124,7 +124,7 @@ It adds a new keyword to the language and increases the language complexity. let dist = Inch(calc_distance(start_inch as int, end_inch as int)); ``` - It also looses type safety. + It also loses type safety. # Unresolved questions From 7565074f2306749a225d8943073db8e971eb3208 Mon Sep 17 00:00:00 2001 From: Jonas Hietala Date: Sat, 26 Jul 2014 17:13:48 +0200 Subject: [PATCH 4/7] Initialize directly from number literals. Clarify and expand. Add notes about exports. --- active/0000-newtype-keyword.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/active/0000-newtype-keyword.md b/active/0000-newtype-keyword.md index d7c9dd7de78..8c54f5482da 100644 --- a/active/0000-newtype-keyword.md +++ b/active/0000-newtype-keyword.md @@ -63,8 +63,8 @@ let oops = inch + cm; // not safe! # Detailed design -Introduce a new keyword: `newtype`. It introduces a new type, with the same -capabilities as the underlying type, but keeping the types separate. +Introduce a new keyword: `newtype`. It introduces a new type, inheriting the +trait implementations from the underlying type, but keeping the types separate. ``` newtype Inch = uint; @@ -75,9 +75,12 @@ fn calc_distance>(start: T, end: T) -> T { end - start } -let (start_inch, end_inch) = (Inch(10), Inch(18)); -let (start_cm, end_cm) = (Cm(2), Cm(5)); +// Initialize the same way as the underlying types +let (start_inch, end_inch): (Inch, Inch) = (10, 18); +let (start_cm, end_cm): (Cm, Cm) = (2, 5); +// Here `calc_distance` operates on the types `Inch` and `Cm`, +// where previously we had to cast to and from `uint`. let inch_dist = calc_distance(start_inch, end_inch); let cm_dist = calc_distance(start_cm, end_cm); @@ -93,9 +96,19 @@ It would also allow generics, like `type`: ``` struct A { n: N, m: M } newtype B = A; + +let b = B { n: 2u, m: "this is a T" }; +``` + +`newtype` would follow the natural scoping rules: + ``` +newtype Inch = uint; // Not accessible from outside the module +pub newtype Cm = uint; // Accessible -Just like `type` does. +use module::Inch; // Import into scope +pub use module::Inch; // Re-export +``` # Drawbacks @@ -126,8 +139,10 @@ It adds a new keyword to the language and increases the language complexity. It also loses type safety. +* Could possibly be implemented as a macro instead, similar to `bitflags!` + # Unresolved questions -None yet +Not sure how to actually implement it. From 853ca3746eaad9cd40c86ac47784152e63a12084 Mon Sep 17 00:00:00 2001 From: Jonas Hietala Date: Mon, 28 Jul 2014 14:41:29 +0200 Subject: [PATCH 5/7] Clarify that newtype would not implement inheritance. Mention GNTD as a useful, possibly preferred, alternative. --- active/0000-newtype-keyword.md | 45 +++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/active/0000-newtype-keyword.md b/active/0000-newtype-keyword.md index 8c54f5482da..fca6ca65000 100644 --- a/active/0000-newtype-keyword.md +++ b/active/0000-newtype-keyword.md @@ -110,14 +110,53 @@ use module::Inch; // Import into scope pub use module::Inch; // Re-export ``` +It would not be possible to use the `newtype` in place of the parent type, +we would need to resort to traits. + +``` +fn bad(x: uint) { ... } +fn good(x: T) { ... } + +newtype Foo = uint; +let a: Foo = 2; +bad(a); // Not allowed +good(a); // Ok, Foo implements Sub +``` + # Drawbacks It adds a new keyword to the language and increases the language complexity. +Automatically deriving all traits may not make sense in some cases. +For example deriving multiplication for `Inch` doesn't make much sense, as it would +result in `Inch * Inch -> Inch` but semantically `Inch * Inch -> Inch^2`. + +This is a deficiency in the design and a better approach may be to explicitly +specify which traits to derive. + # Alternatives +* Explicitly derive selected traits + + Similar to GHC's [`GeneralizedNewtypeDeriving`][newtype-deriving]. E.g.: + + ``` + #[deriving(Sub)] + struct Inch(uint); + + #[deriving(Sub)] + struct Cm(uint); + ``` + + This would avoid the problems with automatically deriving all traits, + while some would not make sense. + + We could save a keyword with this approach and we might consider a generalization + over all tuple structs. + + * Keep it the same It works, but life could be simpler. @@ -139,10 +178,14 @@ It adds a new keyword to the language and increases the language complexity. It also loses type safety. -* Could possibly be implemented as a macro instead, similar to `bitflags!` +* Implement similar behaviour with a macro instead, similar to `bitflags!` + This would not allow us to derive all trait implementations automatically however. + It would work for only primitive types. # Unresolved questions Not sure how to actually implement it. +[newtype-deriving]: https://www.haskell.org/ghc/docs/7.8.1/html/users_guide/deriving.html#newtype-deriving + From f93569b7bd38e6ad0cd0aaeb9588a5d684c6d949 Mon Sep 17 00:00:00 2001 From: Jonas Hietala Date: Wed, 13 Aug 2014 12:18:27 +0200 Subject: [PATCH 6/7] Explicit conversion with as. --- active/0000-newtype-keyword.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/active/0000-newtype-keyword.md b/active/0000-newtype-keyword.md index fca6ca65000..14bab87cf70 100644 --- a/active/0000-newtype-keyword.md +++ b/active/0000-newtype-keyword.md @@ -123,6 +123,22 @@ bad(a); // Not allowed good(a); // Ok, Foo implements Sub ``` +Newtypes can explicitly be casted to their base types, and vice versa. +Implicit conversions should not be allowed. + +``` +newtype Inch = uint; + +fn frobnicate(x: uint) -> uint { x * 2 + 14 - 3 * x * x } + +let x: Inch = 2; +println!("{}", frobnicate(x as uint)); + +let a: uint = 2; +let i: Inch = a; // Compile error, implicit conversion not allowed +let i: Inch = a as Inch; // Ok +``` + # Drawbacks From 212d2d5a8aa4cc0985e96003f7e74975cdcecf40 Mon Sep 17 00:00:00 2001 From: Jonas Hietala Date: Wed, 13 Aug 2014 12:50:25 +0200 Subject: [PATCH 7/7] Clarify semantics with respect to trait derivations. Prettify. --- active/0000-newtype-keyword.md | 74 +++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/active/0000-newtype-keyword.md b/active/0000-newtype-keyword.md index 14bab87cf70..310076d0c9f 100644 --- a/active/0000-newtype-keyword.md +++ b/active/0000-newtype-keyword.md @@ -90,7 +90,7 @@ println!("dist: {} and {}", inch_dist, cm_dist); let not_allowed = calc_distance(start_inch, end_cm); ``` -The grammar rules will be the same as for `type`. + It would also allow generics, like `type`: ``` @@ -100,16 +100,6 @@ newtype B = A; let b = B { n: 2u, m: "this is a T" }; ``` -`newtype` would follow the natural scoping rules: - -``` -newtype Inch = uint; // Not accessible from outside the module -pub newtype Cm = uint; // Accessible - -use module::Inch; // Import into scope -pub use module::Inch; // Re-export -``` - It would not be possible to use the `newtype` in place of the parent type, we would need to resort to traits. @@ -123,6 +113,61 @@ bad(a); // Not allowed good(a); // Ok, Foo implements Sub ``` + +## Derived traits + +In the derived trait implementations the basetype will be replaced by the newtype. + +So for example as `uint` implements `Add`, `newtype Inch = uint` +would implement `Add`. + +This would present a problem when we have a specialization with the same parameter +as a another parameter with a fixed type. For example `trait Shl` where +`RHS = uint` in all implementations. Specifically: + +``` +impl Shl for int +impl Shl for i8 +impl Shl for uint +impl Shl for u8 +... +``` + +`newtype Inch = int` would implement `Shl` but `newtype Inch = uint` +would implement `Shl`, which is not what we want. + +A solution would be to allow `Self` in trait implementations. This would require +us to change `Shl` to: + +``` +impl Shl for int +impl Shl for i8 +impl Shl for uint +impl Shl for u8 +... +``` + +Then `newtype Inch = uint` would implement `Shl`. + +`Self` in trait implementations might require a new RFC. + + + +## Scoping + +`newtype` would follow the natural scoping rules: + +``` +newtype Inch = uint; // Not accessible from outside the module +pub newtype Cm = uint; // Accessible + +use module::Inch; // Import into scope +pub use module::Inch; // Re-export +``` + + +## Casting + Newtypes can explicitly be casted to their base types, and vice versa. Implicit conversions should not be allowed. @@ -140,6 +185,12 @@ let i: Inch = a as Inch; // Ok ``` +## Grammar + +The grammar rules will be the same as for `type`. + + + # Drawbacks It adds a new keyword to the language and increases the language complexity. @@ -199,6 +250,7 @@ specify which traits to derive. This would not allow us to derive all trait implementations automatically however. It would work for only primitive types. + # Unresolved questions Not sure how to actually implement it.