Here're ways to access tuple element in Rust for now:
let x: (i32, f64, u8) = (500, 6.4, 1);
// first
let (a, b, c) = x;
println!("{} {} {}", a, b, c);
// second
println!("{} {} {}", x.0, x.1, x.2);
The first approach is a deconstruct process, which deconstruct the tuple x into a, b and c.
The second approach is to access elements in the tuple x directly.
However, when we use a tuple, it's hard to know meaning of its elements without comments or documents, and things like .0 are hard to distinguish if you lack acknowledge of a tuple.
I proposed name annotation for tuple types:
let x: (count: i32, price: f64, type: u8) = (500, 6.4, 1);
And then we can access elements in the tuple x in this way:
println!("{} {} {}", x.count, x.price, x.type);
It's more intuitive and convenient.
Note that count, price and type are just name annotations for the tuple type, and they don't change the actual type (i32, f64, u8). x.count will be compiled to x.0.
Names are resolved as ways they're declared.
fn bar(v: (a: i32, b: bool)) -> (x: i32, y: bool) {
v.a // ok
v.b //ok
v.u // error, v wasn't declared u
v.v // error, v wasn't declared v
return v;
}
fn main() {
let tup: (u: i32, v: bool) = (5, false);
let mut result = bar(tup);
result.x // ok
result.y // ok
result.a // error, result wasn't declared a
result.b // error, result wasn't declared b
result.u // error, result wasn't declared u
result.v // error, result wasn't declared v
result = tup;
result.x // ok
result.y // ok
result.u // error, result wasn't declared u
result.v // error, result wasn't declared v
}
Is this the same as structural records? https://github.com/rust-lang/rfcs/pull/2584
@SimonSapin I don't think they're same.
You need to declare a record explicitly and then you can use it as "tuple with named fields", whereas tuple is more like an ad-hoc solution for "anonymous records".
I think the ability of "named fields" is also needed for tuple types.
(a: i32, b: f64), (x: i32, y: f64) and (i32, f64) are same with each other, they all have type (i32, f64), and they can have fields with different names in different contexts.
Yes, structural records are anonymous. They鈥檙e not structs.
The difference seems to be that in your proposal, positions/ordering are still meaningful?
(a: i32, b: f64),(x: i32, y: f64)and(i32, f64)are same with each other,
Does this mean that the (a: i32, b: f64) and (b: i32, a: f64) types are also the same? Then how is the something.a expression resolved?
@SimonSapin
It depends. For example:
fn test1() -> (a: i32, b: f64) {
...
}
fn test2() -> (b: i32, a: f64) {
...
}
fn main() {
let x = test1();
let y = test2();
}
In main(), x.a is i32, x.b is f64, y.a is f64 and y.b is i32
This implies that x and y do not have the same type, right? So this wouldn鈥檛 be allowed:
fn main() {
let mut x = test1();
x = test2();
}
In that case I don鈥檛 understand what you meant by:
(a: i32, b: f64),(x: i32, y: f64)and(i32, f64)are same with each other
I believe the labels are merely syntactic sugar here and are not part of the type system.
I believe the labels are merely syntactic sugar here and are not part of the type system.
Yes. It requires no change to existing type system and it just a sugar for compiler and code analysis.
It breaks down on reassignment with conflicting labels though. What do you envision happens then?
let mut foo = (a: 5, b: true);
foo = (x: 7, y: false);
foo.a // ????
@SimonSapin
In that case I don鈥檛 understand what you meant by:
(a: i32, b: f64),(x: i32, y: f64)and(i32, f64)are same with each other
It means the type of (a: i32, b: f64), (x: i32, y: f64) and (i32, f64) are same, they're all (i32, f64) type but with different field name alias.
If they're different types, assigning a (x: i32, y: f64) to a (a: i32, b: f64) variable will failed.
@CryZe
It breaks down on reassignment with conflicting labels though. What do you envision happens then?
let mut foo = (a: 5, b: true); foo = (x: 7, y: false); foo.a // ????
What if we don't allow the syntax let x = (a: 5, b: 1.3), but only allow let x: (x: i32, b:f64) = (5, 1.3)?
Then it will make sense, since you cannot specify name labels on values but only on the type signatures.
I've modified my original proposal.
I believe this still breaks down on reassignment:
fn bar() -> (a: i32, b: bool) { ... }
fn baz() -> (x: i32, y: bool) { ... }
let mut foo = bar();
foo = baz();
foo.a // ????
Unless you can't specify types like this in function return types? That would start to become very inconsistent (also much less useful than structural records).
they're all
(i32, f64)type but with different field name alias.
This is a contradiction to me. Where else would the names exist but as part of the type?
I believe this still breaks down on reassignment:
I think it'd better fix its name labels to the first assignment, this behavior is also used in C# ValueTuple<>.
fn bar() -> (a: i32, b: bool) { ... }
fn baz() -> (x: i32, y: bool) { ... }
let mut foo = bar();
foo = baz();
foo.a // foo.a is i32
foo.b // foo.b is bool
foo.x // compile error
foo.y // compile error
@SimonSapin @CryZe
This is a contradiction to me. Where else would the names exist but as part of the type?
The names of a field in tuple aren't part of a type, they are only language sugar for better code readability and understanding. It can be achieved completely by compiler and code analyzer without any change to existing type system.
Here is a full example:
fn bar(v: (a: i32, b: bool)) -> (x: i32, y: bool) {
v.a // ok
v.b //ok
v.u // error
v.v // error
return v;
}
fn main() {
let tup: (u: i32, v: bool) = (5, false);
let mut result = bar(tup);
result.x // ok
result.y // ok
result.a // error
result.b // error
result.u // error
result.v // error
result = tup;
result.x // ok
result.y // ok
result.u // error
result.v // error
}
That the labels get returned from one function to another like that seems equivalent to "the labels are part of the type" to me. They're still metadata about bar that the compiler needs to know about when compiling main(), and if we replaced bar with an argument of function type, then we could only allow it to have function values that returned the same labels or else it'd be impossible to know what result.x is supposed to compile to.
What I see in this snippet is not an absence of label typing, but implicit coercions between named tuple types that differ only in their names. That raises questions like "does (a: i32, b: bool) coerce to (b: bool, a: i32)? To (y: bool, x: i32)?" and "Does coercion only happen at function returns? It can't be everywhere, or else result.x would likely end up magically accessing result.a or something." Regardless of what precisely a "type" is, those still need answering (and I suspect any _complete_ answer is complex/subtle enough to make the feature not worthwhile).
@Ixrec
When you try to access .a of a (a: i32, b: bool), the compiler compiles it to x.0. My opinion is that the name labels are only aliases to original .0, .1 and etc.
Also, the labels are only inferred from type signature declaration.
I believe the way it could work is if the labels are simply entirely ignored during type checking and only ever considered when accessing tuple fields. And then it uses the labels of the initial declaration of the binding. Not sure if that has any other remaining ugly edge cases, but that at least solves the type problematic for the most part. So you could do:
let foo: (a: i32, b: bool) = (x: 5, y: false);
and it would type check just fine. And foo.a and foo.b are available, not .x or .y. Any reassignments don't change the labels.
Since that didn't really answer the question, let's try making the "function value" case explicit.
What would you want to happen with this code?
fn bar(v: (a: i32, b: bool)) -> (x: i32, y: bool) {
v.a // ?
v.b // ?
v.u // ?
v.v // ?
return v;
}
fn applyToTuple(
f: fn(v: (fa: i32, fb: bool)) -> (fx: i32, fy: bool), // ?
v: (vu: i32, vv: bool)
) -> (g: i32, h: bool) {
f(v) // ?
}
fn main() {
let tup: (u: i32, v: bool) = (5, false);
let mut result = applyToTuple(bar, tup);
result.x // ?
result.y // ?
result.a // ?
result.b // ?
result.g // ?
result.h // ?
result.u // ?
result.v // ?
result = tup;
result.x // ?
result.y // ?
result.a // ?
result.b // ?
result.g // ?
result.h // ?
result.u // ?
result.v // ?
}
fn bar(v: (a: i32, b: bool)) -> (x: i32, y: bool) {
v.a // OK
v.b // OK
v.u // NOT OK, v wasn't declared with having u
v.v // NOT OK, v wasn't declared with having v
// .x and .y aren't ok either
return v;
}
fn applyToTuple(
f: fn(v: (fa: i32, fb: bool)) -> (fx: i32, fy: bool), // OK, labels are ignored in type checking
v: (vu: i32, vv: bool)
) -> (g: i32, h: bool) {
f(v) // OK, labels are ignored in type checking
}
fn main() {
let tup: (u: i32, v: bool) = (5, false);
let mut result /* : (g: i32, h: bool) */ = applyToTuple(bar, tup); // type gets inferred from applyToTuple
result.x // NOT OK, result wasn't declared with having x
result.y // NOT OK, result wasn't declared with having y
result.a // NOT OK, result wasn't declared with having a
result.b // NOT OK, result wasn't declared with having b
result.g // OK
result.h // OK
result.u // NOT OK, result wasn't declared with having u
result.v // NOT OK, result wasn't declared with having v
result = tup;
result.x // NOT OK, result wasn't declared with having x
result.y // NOT OK, result wasn't declared with having y
result.a // NOT OK, result wasn't declared with having a
result.b // NOT OK, result wasn't declared with having b
result.g // OK
result.h // OK
result.u // NOT OK, result wasn't declared with having u
result.v // NOT OK, result wasn't declared with having v
}
Another thing is how they affect type inference in cases like this:
let foo: (_, x: bool) = (a: 5, b: true);
foo.a // OK, foo got inferred as (a: i32, x: bool)
but not
let foo: (i32, bool) = (a: 5, b: true);
foo.a // NOT OK, foo explicitly has no label on the first tuple value.
@CryZe Thanks. It's exactly what I meant.
If there's no more problem with this proposal, I will send a RFC PR soon.
Some more interesting cases:
fn identity<T>(a: T) -> T { a }
fn test() -> i32 {
let x : (a: i32, b: bool) = ...;
// are the names are part of the type argument inferred for the function call?
identity(x).a
}
fn erase_names(v: Vec<(a: i32, b: bool)>) -> Vec<(i32, bool)> {
v // is this OK?
}
In C#, tuple element names are part of the type (at least as far as the C# compiler is concerned; the runtime types are different -- essentially C# has "tuple name erasure").
In addition, there is an "identity conversion" that allows converting between tuple types that differ only in element names.
Also, there is an identity conversion for generic types (even invariant ones, e.g. Vec<A> to Vec<B>) if there is an identity conversion between their type arguments.
The C# compiler will emit a warning when conversion between tuple types with different element names (but not when converting between a tuple type with names and a tuple type without names).
@dgrunwald
fn identity<T>(a: T) -> T { a } fn test() -> i32 { let x : (a: i32, b: bool) = ...; // are the names are part of the type argument inferred for the function call? identity(x).a }
My opinion is: name resolving should based on how the tuple type declared, so names of the return value can be inferred.
fn erase_names(v: Vec<(a: i32, b: bool)>) -> Vec<(i32, bool)> { v // is this OK? }
This is okay but I think the compiler should produce a warning for name losing or name changing.
I do think rustc should eventually support metadata within its type system, but for optional external analysis tools that do formal verification, refinement types, etc., not for syntactic sugar.
If I understand this, there are numerous weaknesses compared with structural records, like type system strangeness and warnings where errors belong, but no real advantages over structural records. It just sweeps the structural record design space under the rug.
We've discussed structural records extensively in #2584 and elsewhere. We've even discussed almost exactly this "order matters" approach briefly I think.
Although #2584 remains open, we've learned structural records interact with far more important type system extensions, like delegation and fields-in-traits. I presume those remain blocked on at least one "in-progress designs and efforts", ala cost generics, specialization, async, chalk, etc.
We also noticed tricks that improve upon structural records for many applications:
fn returns only require that types declared pub inside fn bodies to be usable from outside the fn. It's clear this gives users far more control over the applicable traits too.I think it would be better that way
let unamedstruct: { count: i32, price: f64, type: u8 } = { count: 500, price: 6.4, type: 1 };
or simply
let unamedstruct = { count: 500, price: 6.4, type: 1 };
nothing to do with tuples, starting with tuples having no named elements, structs yes
@cindRoberta I think this is the purpose of RFC #2102.
Julia uses it with NamedTuple
Most helpful comment
Is this the same as structural records? https://github.com/rust-lang/rfcs/pull/2584