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

Proposal: Name annotation for tuple types #2870

Open
hez2010 opened this issue Feb 26, 2020 · 27 comments
Open

Proposal: Name annotation for tuple types #2870

hez2010 opened this issue Feb 26, 2020 · 27 comments

Comments

@hez2010
Copy link

hez2010 commented Feb 26, 2020

Background

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.

Proposal

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.

Name resolving

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
}
@SimonSapin
Copy link
Contributor

Is this the same as structural records? #2584

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

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

@SimonSapin
Copy link
Contributor

SimonSapin commented Feb 26, 2020

Yes, structural records are anonymous. They’re 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?

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

@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

@SimonSapin
Copy link
Contributor

This implies that x and y do not have the same type, right? So this wouldn’t be allowed:

fn main() {
  let mut x = test1();
  x = test2();
}

In that case I don’t understand what you meant by:

(a: i32, b: f64), (x: i32, y: f64) and (i32, f64) are same with each other

@CryZe
Copy link

CryZe commented Feb 26, 2020

I believe the labels are merely syntactic sugar here and are not part of the type system.

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

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.

@CryZe
Copy link

CryZe commented Feb 26, 2020

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 // ????

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

@SimonSapin

In that case I don’t 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.

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

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

@CryZe
Copy link

CryZe commented Feb 26, 2020

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

@SimonSapin
Copy link
Contributor

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?

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

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

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

@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
}

@Ixrec
Copy link
Contributor

Ixrec commented Feb 26, 2020

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

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

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

@CryZe
Copy link

CryZe commented Feb 26, 2020

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.

@Ixrec
Copy link
Contributor

Ixrec commented Feb 26, 2020

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 // ?
}

@CryZe
Copy link

CryZe commented Feb 26, 2020

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.

@hez2010
Copy link
Author

hez2010 commented Feb 26, 2020

@CryZe Thanks. It's exactly what I meant.

@hez2010
Copy link
Author

hez2010 commented Feb 27, 2020

If there's no more problem with this proposal, I will send a RFC PR soon.

@dgrunwald
Copy link
Contributor

dgrunwald commented Feb 28, 2020

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

@hez2010
Copy link
Author

hez2010 commented Feb 29, 2020

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

@burdges
Copy link

burdges commented Mar 8, 2020

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:

  • Ad-hoc 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.
  • We've the structural, refraction, and generic-field-projection crates that provide the has-field component of structural records. And do a better job sometimes. Also, I suppose rustc could eventually bless some has-field traits for use with FRU, except that also sounds excessively magical.

@igotfr
Copy link

igotfr commented Apr 15, 2020

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

@iago-lito
Copy link

iago-lito commented Apr 15, 2020

@cindRoberta I think this is the purpose of RFC #2102.

@igotfr
Copy link

igotfr commented Sep 4, 2020

Julia uses it with NamedTuple

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants