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

First draft of tuples design doc #111

Closed
wants to merge 26 commits into from
Closed

First draft of tuples design doc #111

wants to merge 26 commits into from

Conversation

josh11b
Copy link
Contributor

@josh11b josh11b commented Jul 14, 2020

Tuples in Carbon play a role in several parts of the language:

  • They are a light-weight product type.
  • They support multiple return values from functions.
  • They provide a way to specify a literal that will convert into a struct value.
  • They are involved in pattern matching use cases (such as function signatures)
    particularly for supporting varying numbers of arguments, called "variadics."
  • A tuple may be unpacked into multiple arguments of a function call.

@googlebot googlebot added the cla: yes PR meets CLA requirements according to bot. label Jul 14, 2020
@josh11b josh11b added proposal A proposal WIP labels Jul 14, 2020
var (||): e = (||);
```

**Oddity:** The 0-tuple type should also be able to be written `struct {}`, but
Copy link

Choose a reason for hiding this comment

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

should or can?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know yet, I've added the sentence:

We will need to resolve this contradiction somehow.

```

The basic question here is tuple enough like an array to use the same operator
to access its elements?
Copy link

Choose a reason for hiding this comment

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

I like [| |] as it has a nice property of reminding readers what is going on

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ultimately I decided this syntax is too unfamiliar, too visually noisy, and not a common enough use case to justify a separate operator.

```
// Define a function that takes keyword arguments.
// Keyword arguments must be defined after positional arguments.
fn f(Int: p1, Int: p2, .key1 = Int: key1, .key2 = Int: key2) { ... }
Copy link

Choose a reason for hiding this comment

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

I kinda hate the repetition of .key1 = Int: key1... not sure what to do about it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. I think this is one argument in favor of name: type instead of type: name. In the name: type convention, you could write something like .name: type to say "name is keyword argument". Might have the opposite problem of being too subtle though.


We are currently making order always matter for [consistency](#order-matters),
even though the implementation concerns for function calling may not require
that particular constraint.
Copy link
Contributor

@mconst mconst Jul 15, 2020

Choose a reason for hiding this comment

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

Hmm, this doesn't seem great for argument forwarding. Say you want to forward all your arguments to another function, along with an extra keyword argument .foo = bar. If keyword arguments can be passed in any order, it just works:

SomeFunction(args..., .foo = bar);

But if we enforce ordering, that natural-looking code will break any time the user passes a keyword argument that happens to come after .foo in SomeFunction's declaration order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that is a strong argument, but I'm having trouble balancing it against the consistency argument. In particular, it seems like the restrictions should be the same when constructing a struct vs. calling a function, since the struct name acts just like a function returning a value of its type, except that we will likely have less control of destruction order for the struct case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now, I've just added your concern to the document.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good!

When you added the sentence "We may need to allow keyword arguments in any order to allow use cases with unpacking", was it intended to replace the following sentence "Hmm, this doesn't seem great for argument forwarding"? The way it reads right now is kind of confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I was interrupted in the middle of putting this change in and I lost track of what I was trying to do. Hopefully better now.

docs/design/tuples.md Outdated Show resolved Hide resolved

## Equality

Tuple types don't have names, so they are compared structurally. That means two
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean that two user-defined types may be comparable with one another even if neither author intended this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To get a non-structural type, you give it a name. Tuples don't have names, but they can be converted to structs that match structurally. So as long as your user-defined types are named, then they won't be comparable, but you could have a non-transitive situation like:

struct A {
  var Int: x;
}
struct B {
  var Int: x;
}
StaticAssert(A != B);
var A: a = (| .x = 1 |);
var B: b = (| .x = 1 |);
Assert(a == (| .x = 1 |));
Assert(b == (| .x = 1 |));
// Compile-time type error: Assert(a == b);

Comment on lines 88 to 92
One thing we have considered is using a type constructor to name tuple types, so
the type of `(|1, 2.0, true|)` would be `Tuple(Int, Float64, Bool)` instead of
`(|Int, Float64, Bool|)`. Another way we could spell these types is by equating
tuples and unnamed structs, but this would require a syntax for defining unnamed
fields in structs accessed positionally.

Choose a reason for hiding this comment

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

I've been thinking of the tuple literal (|stuff|) syntax as a shorthand for Tuple(stuff). I find that way of thinking helpful, because then tuples are just like any other type, but we decided that they are common enough that they need a simpler syntax. This also gives us a benchmark for what our alternate syntax needs to be better than.

I also like the idea of unifying tuples and structs, because it means I define a struct with useful names for the fields, but still use it generically as a tuple. If we have that facility on structs, then the only thing I need at that point is a way to be able to leave a field unnamed. I seem to recall other areas in the language where we want a placeholder for things that need a name syntactically, but not semantically. Then, rather than Tuple(Int, Double, Bool) being the base syntax we are competing against, it becomes something more like struct { Int _; Double _; Bool _; } (or whatever syntax we decide on for struct declarations). For the value case, the "base" syntax becomes something like struct { Int _; Double _; Bool _; }(1, 2.0, true). In some ways, tuples then become more special, but in other ways, they are not a distinct thing because they are just a generalization of a fundamental language feature. The trick here is that the tuple types must have structural equality -- two declarations of struct { Int _; } must actually declare the same type. I'm not entirely sure how to resolve that in a way that doesn't cause more problems elsewhere, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our current rule is that structs use structural equality if they don't have names. This means
struct { var Int: x; var Int: y; } and Tuple(.x = Int, .y = Int) are actually equal types. If we adopted your _ convention for defining positional fields in structs, then
struct { var Int: _; var Double: _; var Bool: _; } would be equal to Tuple(Int, Double, Bool).

However, struct GivenAName { var Int: x; var Int: y; } is a different type than Tuple(.x = Int, .y = Int), but we allow you to convert values of the latter into values of the former.

Comment on lines +152 to +156
If we provide some way of defining positional fields for structs (which we
haven't done so far, but would allow us to make tuples a special case of
structs), then it would be convenient to access those positional field using
`[|index|]` to avoid interfering with any operator `[]` you might want to define
for that struct type.

Choose a reason for hiding this comment

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

This is an interesting argument.


## Multiple indices

Pass tuple of indices to a tuple to get another tuple:

Choose a reason for hiding this comment

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

If I pass a tuple of indexes to an array, do I get an array back? Or do I pass an array of indexes to the array?

If we want to support this syntax, it makes sense for the tuple case to accept a tuple of indexes because then I could do x[(|2, Int|)] to index positionally and by type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In general, I would expect you to get a foo back from passing a foo to a bar for foo, bar in { array, tuple } (with the caveat that not all combinations may be defined). I think this follows from the fact that the result should have a fixed size if the argument to [...] has a fixed size.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a brief mention to the doc below:

The rule is that the expression inside the [...] would determine the structure
of the resulting value.

(|x, y|) = Position(that_point);
```

## Named members

Choose a reason for hiding this comment

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

This further blurs the line between tuples and structs, and makes me further wonder whether one can just be a simpler syntax for the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I discuss the relationship between tuples and structs in the tuple doc, which I will update soon to reflect the fact that I've copied a bunch of content from the struct doc into this doc since it makes more sense here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I meant to say, this is discussed in the struct doc. I have since deleted content from the struct doc that I had moved here.

Comment on lines 315 to 317
We may need to provide a way for users to explicitly specify a struct layout and
an explicit (different) order of initialization for the rare structs where this
matters.

Choose a reason for hiding this comment

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

If we unify tuples and structs, more advanced use cases like these can always fall back on the "regular" full struct syntax and leave the simple syntax simple.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I've changed the text.

docs/design/tuples.md Show resolved Hide resolved
docs/design/tuples.md Show resolved Hide resolved
arguments are matched by the `...`. Is it influenced by <Type>? Or the next
thing in the parameter list?

We shall also define a type `NTuple(Int:$$ N, Type:$$ T)` which is equivalent to
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think NTuple is a type, because it has parameters. It's also not a parameterized type, because that would imply that NTuple(1, Int) and (|Int|) are different types, which would defeat the purpose. It seems more like a function (i.e. given N and T, it returns a tuple consisting of N copies of T), but presumably we don't want to open the can of worms of allowing pattern-matching to deduce the parameters of a function from its return value. My best guess is it's a parameterized type alias; after all, C++ allows deduction through aliases in precisely this way. However, that relies on the fact that there are very tight restrictions on the structure of an alias definition, and it seems doubtful that we can relax those restrictions enough to permit defining something like NTuple. So my best guess is that NTuple is a special alias whose definition is "magical", but that doesn't seem very satisfying.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added a discussion about NTuple specifically to the pattern matching doc, though it still has more questions than answers.

docs/design/tuples.md Outdated Show resolved Hide resolved
docs/design/tuples.md Outdated Show resolved Hide resolved
docs/design/tuples.md Outdated Show resolved Hide resolved
docs/design/tuples.md Outdated Show resolved Hide resolved
This is covered in more detail in
[the structs design doc](https://github.com/josh11b/carbon-lang/blob/structs/docs/design/structs.md#simple-initialization-from-a-tuple).

## Function calling
Copy link
Contributor

Choose a reason for hiding this comment

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

There's an alternative model we could have, where function arguments literally are just a tuple, rather than having explicit pack/unpack operations to convert between tuples and argument lists. That seems like it would provide some important simplifications. I know we've discussed this before and provisionally decided against it, but I have trouble recalling the reasons, so I think it's important for this proposal to explicitly discuss and justify that decision.

```

**Concern** from [mconst](https://github.com/mconst):
Copy link
Contributor

Choose a reason for hiding this comment

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

I tend to agree with @mconst on this. In any event I'd recommend deferring it to a followup proposal, in the interests of minimizing PR size, unless you think this is an important constraint on other aspects of the tuple design.

that?

### Concern
#### Syntax concern

[geoffromer](https://github.com/geoffromer) says:
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless I'm misunderstanding what I meant by this comment, I now think this is completely wrong, for the reasons laid out in the comment you quote below. Maybe just delete this?

Comment on lines +582 to +584
[geoffromer](https://github.com/geoffromer) also brings up that it would be
consistent to use the `...` unpacking operator on a tuple type in a pattern to
represent variadics [slightly edited]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
[geoffromer](https://github.com/geoffromer) also brings up that it would be
consistent to use the `...` unpacking operator on a tuple type in a pattern to
represent variadics [slightly edited]:
[geoffromer](https://github.com/geoffromer) also brings up that it would be
consistent to use the postfix `...` unpacking operator on a tuple type in a pattern to
represent variadics (rather than a separate pattern-only prefix operator)[slightly edited]:

@@ -562,6 +579,50 @@ that?
> syntax. I don't know of any good precedents, unfortunately- `**` seems
> unworkable because of ambiguity with double-dereferencing.

[geoffromer](https://github.com/geoffromer) also brings up that it would be
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worth linking directly to the comment, so people can get additional context. It might even make sense to only provide the link (and a quick summary), rather than quoting the whole comment.

```

The rule is that the expression inside the `[...]` would determine the structure
of the resulting value.

## Slicing
Copy link
Contributor

Choose a reason for hiding this comment

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

As with multi-indexing, I'd recommend deferring this to a followup proposal. It seems like something we don't need to resolve in the first pass, because it'll probably have relatively few interactions with other parts of the tuple design, or other parts of the language.

@jonmeow jonmeow marked this pull request as draft April 20, 2021 16:20
@jonmeow jonmeow removed the WIP label Apr 20, 2021
@github-actions
Copy link

github-actions bot commented Aug 2, 2021

We triage inactive PRs and issues in order to make it easier to find active work. If this PR should remain active, please comment or remove the inactive label.
This PR is labeled inactive because the last activity was over 90 days ago. This PR will be closed and archived after 14 additional days without activity.

@github-actions github-actions bot added the inactive Issues and PRs which have been inactive for at least 90 days. label Aug 2, 2021
@github-actions
Copy link

We triage inactive PRs and issues in order to make it easier to find active work. If this PR should remain active or becomes active again, please reopen it.
This PR was closed and archived because there has been no new activity in the 14 days since the inactive label was added.

@github-actions github-actions bot closed this Aug 17, 2021
@github-actions github-actions bot added the proposal deferred Decision made, proposal deferred label Jul 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes PR meets CLA requirements according to bot. inactive Issues and PRs which have been inactive for at least 90 days. proposal deferred Decision made, proposal deferred proposal A proposal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants