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

RFC: Tuple indexing #184

Merged
merged 6 commits into from
Sep 3, 2014
Merged

RFC: Tuple indexing #184

merged 6 commits into from
Sep 3, 2014

Conversation

ftxqxd
Copy link
Contributor

@ftxqxd ftxqxd commented Jul 24, 2014


```rust
let x = (box 1i, box 2i);
let x1 = { let (ref a, _) = x; a };
Copy link

Choose a reason for hiding this comment

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

It's not if the indexing is zero based.

@dhardy
Copy link
Contributor

dhardy commented Jul 24, 2014

Wouldn't it be better to write a struct at the point you want to store and extract values from a tuple as a conglomerate? As I understand it, tuples are only really useful as a light-weight way of moving multiple values out of (and sometimes into) functions.

Allowing x.1 might lead to people wanting x.n. If it comes to generic code, I think macros like tuple_get(x, n) would be preferable.

@chris-morgan
Copy link
Member

👎: if you’re wanting to do things like this, you probably shouldn’t be using tuples. Personally I feel that it would be justifiable to give the guidance that tuples should only be used for simple generic arguments (where a function may accept a Foo or a (Bar, Baz) in a given position), and for returning multiple values from a function—possibly even only the second. In each of these cases, unpacking with a full pattern match is entirely reasonable, probably even desirable.

I would actually rather like Tuple1Tuple12 to be removed; I feel them superfluous, and they steer people away from using the pattern matching, leading frequently to bad code.

@lilyball
Copy link
Contributor

👍 I think this is useful. I think it should also work on tuple-structs (e.g. struct Foo(int,char);).

@lilyball
Copy link
Contributor

@dhardy You cannot write a macro like that. Macros don't know about types, so you cannot construct a proper destructuring pattern for the tuple. Even if you could, such a macro could then not produce an lvalue to allow for assignment, which I believe this syntax should allow, e.g.

let mut x = (1u, 2i, '3');
x.0 = 42u;

For reference, Swift allows this tuple indexing syntax, and allows for assignment with it.

@dhardy
Copy link
Contributor

dhardy commented Jul 24, 2014

@kballard I don't think Rust should include features just because Swift does. I agree with CM on this.

@dobkeratops
Copy link

+1 swift has a lot of nice pragmatic tweaks, and this is one of them, IMO.

this would be particularly useful with tuple-structs. There area times when the name of the type , and the types or positions of the components are sufficient information, and field names are just clutter. (vica versa, I would also like to see anonymous structs, but more ergonomic tuple structs would cover some of those cases).

tuple structs also have the draw that the constructor is nice, but the repulsion that they're currently awkward to use (you must repeat the typename for destructuring).

you can roll accessors with a macro but they also clutter your code and make it look more expensive than it is (and those macro definitions are another dependancy for libraries)

Tangential but related,
I would also really like to see struct-member/method delegation with tuples (prioritise by order)..tuples are great when you haven't yet figured out precisely which subsets of some data structure are needed by which areas of code.. you can keep the arguments split, and pass different subsets down. (this would be a bit like 'intersection types'?).

Committing to names before you've explored a problem is tiresome

@netvl
Copy link

netvl commented Jul 24, 2014

+1. This would be very convenient, especially, as others have already said, with tuple structs.

@lilyball
Copy link
Contributor

Incidentally, I don't think it makes sense to define this RFC in terms of the equivalent destructuring code. That just raises questions on edge cases that haven't been considered (or even not-so-edge cases, like assignment to a tuple index). I think it makes much more sense just to define tuple indexing as equivalent to using named fields, so the first field in a tuple implicitly has the field name 0 and access to that field works just like accessing a named field in a struct. This definition also has the benefit of making it more obvious that x.n doesn't make sense (no more than let s = "foo"; x.s does for a struct).

@japaric
Copy link
Member

japaric commented Jul 24, 2014

Big 👍

Indexing on tuple structs makes the newtype idiom much easier to write. I'd be able to replace all those ugly let &Sample(ref sample) = self;s in my code with just self.0. Yes, please!

@huonw
Copy link
Member

huonw commented Jul 24, 2014

Indexing on tuple structs makes the newtype idiom much easier to write. I'd be able to replace all those ugly let &Sample(ref sample) = self;s in my code with just self.0. Yes, please!

FWIW, you can get this now by just using a normal struct with a field name: struct Sample { data: ... } ... &self.data. (This doesn't differ at runtime to a tuple "newtype".)

@glaebhoerl
Copy link
Contributor

I think it makes much more sense just to define tuple indexing as equivalent to using named fields, so the first field in a tuple implicitly has the field name 0 and access to that field works just like accessing a named field in a struct. This definition also has the benefit of making it more obvious that x.n doesn't make sense (no more than let s = "foo"; x.s does for a struct).

Yep. I think this is the right idea, and the proposal is just closing a features gap with respect to structs-with-named-fields.

tuple structs. This syntax is recognised wherever an integer or float literal is
found in place of the normal field or method name expected when accessing fields
with `.`. Float literals in this position are expanded into two field accesses,
so that an expression of the form `a.1.3` is equivalent to `(a.1).3`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that a.1.3 should be legal and equivalent to (a.1).3, but isn't there a way to do this without introducing the awkwardness of float literals in the first place? I.e. just say that valid tokens following the . operator are either an identifier (named struct field, method) or a decimal number? And a.1.3 is just the same thing occuring twice.

Choose a reason for hiding this comment

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

@glaebhoerl I would imagine (I am not well aquainted with the internals of the rust compiler) that float literals become involved because this RFC would probably be implemented by modifying the parser, whereas float literals are a type of token output by the lexer (since the lexer typically doesn't have much of a notion of context).

Copy link
Contributor

Choose a reason for hiding this comment

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

+1


Allow indexing of tuples and tuple structs: this has the advantage of
consistency, but the disadvantage of not being checked for out-of-bounds errors
at compile time.
Copy link
Contributor

Choose a reason for hiding this comment

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

As also pointed out on reddit, indexing as such (i.e. tuple[0], tuple[1] etc.) wouldn't make sense as an alternative, because tuples are heterogenous. (They definitely couldn't be made to implement the Index* traits.)

@bachm
Copy link

bachm commented Jul 25, 2014

👍 this seems to be a no brainer ergonomics improvement.

@chris-morgan
Copy link
Member

@bachm: yes, it improves the ergonomics of using tuples—but do we want to be doing that? I do not believe that we should be: such techniques are not good for code readability; I believe that they should always (yes, always—I will not even use Tuple1Tuple12) be replaced with total destructuring of the tuple, or the use of structs.

@bachm
Copy link

bachm commented Jul 26, 2014

@chris-morgan yes, that also make sense from a certain point of view. However, currently Tuple1 etc. exist and if they stay, we might as well use the shorter form proposed here.

@chris-morgan
Copy link
Member

@bachm: I would say it the other way round: we have Tuple1Tuple12 in our libraries and they work, so why add further complexity to the language in this way?

@pczarn
Copy link

pczarn commented Jul 26, 2014

Tuples and structs are not as alike as you think. Tuples probably can't have layout optimization. They are heterogeneous sequences, not records for accessing fields.

@Valloric
Copy link

I have to say @chris-morgan has a good point. In my experience, people tend to overuse tuples to the point where it hurst readability. A struct with named fields is almost always a better idea, and in Rust we have destructuring which makes pulling out relevant parts of tuples from a function returning a tuple really easy.

So while this change is an ergonomic improvement, by implementing it we might be steering people to sub-par designs.

@glaebhoerl
Copy link
Contributor

So while this change is an ergonomic improvement, by implementing it we might be steering people to sub-par designs.

The argument that a feature can be abused to write code with bad readability, and that therefore, it's better to not have the feature, is unpersuasive to me in general. It's true of most features. I prefer to leave issues of style and taste up to the programmer, because "one size fits all" rules generally don't. The proper avenues for encouraging good style, in my opinion, are through example-setting, cultural norms, education & community pressure, style guides for individual projects, etc. I.e. not by restricting the expressiveness or convenience of the language itself.

People can write this today:

let mut ref mut a = b;

yet, remarkably, they don't.

A solid argument against the feature is if you believe that it's outright never a good idea to use it (as I think @chris-morgan might), in which case I would like to see some compelling evidence or reasoning to back that up.

@dobkeratops
Copy link

If you could destructure 'self', in the signature, that might remove some of the demand for this feature. but thanks to swift there's going to be a large community familiar with the .0 .1 ... notation

One nice thing about tuples is avoiding needing to manage a dependancy. if two libraries use a common structure, which should own it ? - they can make themselves compatible without clashing on a declaration

@lilyball
Copy link
Contributor

@chris-morgan Tuple1...Tuple12 work for tuples, but they don't work for tuple structs. That I think is the best reason to add this to the language; right now, tuple-structs are rarely used because the ergonomics of using them are terrible. But the ergonomics of constructing them are actually quite good, and it's a shame we have to avoid them because we can't use them well.

@emberian
Copy link
Member

Tuple structs (and tuple variants) are also annoying because they are very
hard to document. Sometimes you really want a name...

On Sun, Jul 27, 2014 at 11:49 AM, Kevin Ballard notifications@github.com
wrote:

@chris-morgan https://github.com/chris-morgan Tuple1...Tuple12 work for
tuples, but they don't work for tuple structs. That I think is the best
reason to add this to the language; right now, tuple-structs are rarely
used because the ergonomics of using them are terrible. But the ergonomics
of constructing them are actually quite good, and it's a shame we have to
avoid them because we can't use them well.


Reply to this email directly or view it on GitHub
#184 (comment).

http://octayn.net/

@bluss
Copy link
Member

bluss commented Jul 27, 2014

With Indexing traits finally on board, I've implemented matrices that index by tuples -- Abc[(1,2)] = 1.0 and so on. When indexing you don't want to create anything more complex than a tuple to describe your index. Anyway; when implementing this, tuple field names are convenient.

@lilyball
Copy link
Contributor

Tuple structs (and tuple variants) are also annoying because they are very
hard to document. Sometimes you really want a name...

Yeah, in most cases you'll want a named field. But sometimes you don't. Most obvious case is multiple return values from a function; generally you'll want a tuple, not a struct.

Indexing of tuples makes no sense because not all tuples are homogenous.
@omasanori
Copy link

I agree with @chris-morgan.

Most obvious case is multiple return values from a function; generally you'll want a tuple, not a struct.

Indeed but we can use patterns to name each values, as already noted in the Alternatives section.

let (quot, rem) = num::div_rem(n, m);
println!("The remainder is {}.", rem);
// vs
let res = num::div_rem(n, m);
println!("The remainder is {}.", res.1);

The latter is shorter but I prefer the former for readability.

Also, an example by @kballard:

let mut x = (1u, 2i, '3');
x.0 = 42u;

It is cool but I prefer using variables and constructing a tuple when we need it (to pass to a function, as a return value, etc.)

@lilyball
Copy link
Contributor

I prefer using variables and constructing a tuple when we need it (to pass to a function, as a return value, etc.

So do that. I'm not suggesting that you should be storing local values in a tuple. That snippet is not supposed to represent real code but instead supposed to demonstrate a reduction of real code.


Fundamentally, a tuple is a heterogenous fixed-length collection. Similarly, an array is a homogenous fixed-length collection. Why should the latter allow indexing but not the former?

@ftxqxd
Copy link
Contributor Author

ftxqxd commented Jul 28, 2014

@omasanori You’ve made me realise another point in favour of this RFC. If a function returns a tuple, quite often only one value of that tuple is needed. Your div_rem example could be rewritten in one line with this RFC like so:

println!("The remainder is {}.", num::div_rem(n, m).1);

This is obviously not a good example, because one can just use % instead, but in the vast majority of cases there is no equivalent single-value function.

Now even if there aren’t many functions that return tuples (I don’t know how many there are), a feature like this could allow more functions that return tuples. Right now a function that returns a tuple can be quite difficult to use—one must assign each value in a let, even if one only needs one of the values. This RFC makes such functions much easier to use, and thereby makes defining a function that returns a tuple less of a bad idea.


Personally, I think that the best alternative (or even complement) to this RFC would be anonymous structs. They are a great way to give names to return values without having to define a whole new struct (std::str::CharRange is a good example of a struct which I consider unnecessary). However, unless we get anonymous structs, the best way to describe (in code) what values are in a tuple is to put the descriptions in the name of the function itself: div_rem is an example of something that already does this. (Maybe char_range_at could be renamed to char_next and made to return a tuple?)

@omasanori
Copy link

@kballard Thank you for clarification.
I feel, especially in statically-typed languages, the types of a[1], a[2], ..., a[N] should be the same. Even in Lisp, I sometimes consider using structs or defining getterd and setters for such heterogeneous collections instead of using N-th accessors.

@P1start It sounds good but personally I still prefer

let (_, rem) = num::div_rem(n, m);

style. Indeed such shorthands must be loved by some people, though. Anonymous structs might be good too,

@lilyball
Copy link
Contributor

@omasanori

I feel, especially in statically-typed languages, the types of a[1], a[2], ..., a[N] should be the same.

Yes, that's why nobody is advocating for adding indexing syntax to tuples. The tuple.1 syntax is a much better fit; it's basically anonymous field access, rather than indexing.

@lilyball
Copy link
Contributor

FWIW, I just attempted to use a tuple-struct containing a &mut reference and I could not figure out how to appease borrowck into allowing me to reborrow the contained &mut. Tuple-indexing would fix that.

Sample code:

struct TupleStruct<'a>(&'a mut String);

impl<'a> TupleStruct<'a> {
    fn inner(&mut self) -> &mut String {
        let TupleStruct(ref mut s) = *self;
        // the following complains that the lifetime of `s` is too short
        &mut **s
    }
}

Edit: Figured it out:

struct TupleStruct<'a>(&'a mut String);

impl<'a> TupleStruct<'a> {
    fn inner(&mut self) -> &mut String {
        let TupleStruct(&ref mut s) = *self;
        s
    }
}

@Kimundi
Copy link
Member

Kimundi commented Jul 31, 2014

+1 from me.
Tuples and tuple structs are just structs with anonymous fields ordered by their definition, so both should support syntax for accessing a field as an lvalue directly to make working with them more consistent and easier.

@ftxqxd
Copy link
Contributor Author

ftxqxd commented Aug 10, 2014

I’ve made a functioning prototype implementation of this in my tuple-indexing branch.

@nikomatsakis
Copy link
Contributor

I've been wanting to propose something like this for a while. I much prefer &tuple.0 to tuple.ref0()

@nikomatsakis
Copy link
Contributor

Also: I'd rather not change the lexer to permit a.0.1. I'd rather just have that be an error and have people write out the names. We could always add it later.

@brson
Copy link
Contributor

brson commented Aug 26, 2014

Discussed in https://github.com/rust-lang/meeting-minutes/blob/master/weekly-meetings/2014-08-26.md

@P1start please ammend the RFC to remove the surface grammar that converts floats to field access (but do leave a note about the compromise). This is a complexity that we don't believe is worth contorting the grammar for. Also please add a feature gate ('tuple_indexing' perhaps).

@ftxqxd
Copy link
Contributor Author

ftxqxd commented Aug 27, 2014

@brson I’ve amended the RFC.

@brson
Copy link
Contributor

brson commented Sep 3, 2014

@P1start thanks!

@brson brson merged commit 9f146c1 into rust-lang:master Sep 3, 2014
@brson
Copy link
Contributor

brson commented Sep 3, 2014

Merged as RFC 53. Tracking bug: rust-lang/rust#16950

@asterite
Copy link

asterite commented Sep 4, 2014

I think adding a new syntax for this is unnecessary. This should work:

let mut x = (1u, 2i);
let y = x[0];
x[0] = 2u;

No need to add new syntax. But, the semantic checker must know that x is a tuple, that it is being indexed (or indexed-assigned) with an integer literal. If it's not an integer literal, compile-time error.

Pros:

  • New syntax is not needed
  • The syntax is more familiar and intuitive (at least it's very common in many other languages)

Cons:

  • Special logic nees to be added to some semantic pass, but it should be trivial to implement.

@lilyball
Copy link
Contributor

lilyball commented Sep 4, 2014

@asterite Indexing syntax was already commented upon, and rejected, because it's actually a really bad fit. Notably, indexing syntax everywhere else has a consistent type, but a tuple is heterogenous so a[0] and a[1] would have different types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-expressions Term language related proposals & ideas A-tuples Proposals relating to tuples.
Projects
None yet
Development

Successfully merging this pull request may close these issues.