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: expose tuple type declarations #4335

Closed
fengb opened this issue Jan 30, 2020 · 48 comments · Fixed by #13627
Closed

Proposal: expose tuple type declarations #4335

fengb opened this issue Jan 30, 2020 · 48 comments · Fixed by #13627
Labels
accepted This proposal is planned. contributor friendly This issue is limited in scope and/or knowledge of Zig internals. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@fengb
Copy link
Contributor

fengb commented Jan 30, 2020

We now have anonymous tuples, but they can only be created at runtime using the var type. While this satisfies "varargs" and coercion to arrays / vectors, we still cannot define a tuple type at comptime.

Potential syntax:

const Point = tuple{ i32, u32 }; // explicit, but adds a keyword
const Point = struct{ @"0": i32, @"1": u32 }; // matches the backend structure but feels pretty gross

Packed tuples should also be possible since it can represent in memory data structures pretty well, like manually managed stacks.

Accepted syntax

Per #4335 (comment):

const Point = struct{ i32, u32 };
@fengb
Copy link
Contributor Author

fengb commented Jan 31, 2020

Another usecase for declared tuples: emulating multiple returns.

@andrewrk andrewrk added this to the 0.6.0 milestone Jan 31, 2020
@andrewrk andrewrk added accepted This proposal is planned. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. labels Jan 31, 2020
@metaleap
Copy link
Contributor

Do you foresee another .Tuple (vs .Struct) TypeInfo / TypeId for that (would intuitively advise against such) or have it more of a sugar building on the existing struct internals incl the existing "0", "1" etc auto-naming?

@andrewrk
Copy link
Member

@typeInfo for tuples is the same as structs, but there will be an added flag is_tuple: bool

@metaleap
Copy link
Contributor

metaleap commented Jan 31, 2020

Pondering it some more..

Hate to bikeshed, truly do usually, but would propose an alternative to "yet another keyword": extend the lang syntax such that struct {i32, bool, void} becomes valid and parses into an is_tuple struct (as we know it exists underlying already from .{10, "hi", false} literals) --- with the easy-to-intuit rule that either all fields must be named or all must be unnamed. If the parser can be made to do tuple{x,y,...} surely it can be made to do struct{x,y,...} =) whether the default-values part in a StructFieldNode (or however it's called internally) following a = should stay in here or go, I can't assess well right now but the same question would be for a tuple{} keyword.

What bugs me about a new keyword? One subtle quality (among various) I liked about Zig is that there is kinda mostly / overall / in-spirit one moniker for one underlying-mechanism/operational-semantic, so to speak. Now you'd have to explain in docs/chats/blogs/teaching-materials/talks at length "there are unions, they are like this, then there are structs, they are like that, and btw but there's also another special kind of structs without field names, so there are in addition to unions and structs also tuples but they're really structs under the hood".

PLT folks love the "tuple" moniker but it was always understood that structs/records/classes/tuples are really all primarily one thing, product types. If a struct can have or lack a certain (programmer-facing only, no difference to machine or end user) quality of trivial difference, another keyword for "almost the same thing, except names" might be disproportionate.

@andrewrk
Copy link
Member

@metaleap 's ammendum #4335 (comment) is also accepted

@Rocknest
Copy link
Contributor

I don't understand what is being proposed. Is it a special way to declare struct without specifying field names? What is a packed tuple, i can't imagine that, there are no examples to visualise it.

As for use case "emulating multiple returns": i would expect multiple returns to be named, as arguments to a function must have names. e.g:
getPosition() struct {x: f32, y: f32}
divide(num: u64, denom: u64) struct {result: u64, remainder: u64}
getRegs() struct {bp: usize, ip: usize}

Also would anon list literals be coerceable to 'tuples'? @LemonBoy is saying that it should not be allowed #4148 (comment).

@LemonBoy
Copy link
Contributor

I don't understand what is being proposed.

Nor you have understood what I was saying as I was talking about coercing anonymous array literals to named struct literals where you really want to specify what field are you assigning a value to.

@Rocknest
Copy link
Contributor

Rocknest commented Jan 31, 2020

@LemonBoy your argument that it is a time-bomb does not justify your point, however i understand why it would not be allowed - "favour reading code over writing code"

Edit: how do 'tuples' fit into this zen?

@metaleap
Copy link
Contributor

metaleap commented Jan 31, 2020

Edit: how do 'tuples' fit into this zen?

@Rocknest understand this: we already have tuple literals: .{ 10, "20", 3.0 } (that creates under the hood the appropriate struct type with 3 fields named "0" through "2"), just no way to express in code the type of such a thing in current syntax. (For args, vars, consts, fields, you name it.)

The proposal is to have such a facility in the grammar.

@Rocknest
Copy link
Contributor

@metaleap as i said #4335 (comment) i wonder what the use case would be, is that really justified?

@Sobeston
Copy link
Contributor

I feel like a separate key word may cause more confusion than it's worth - new users would likely see it and assume it's a different data structure. I like the idea of making a struct type without specifying field names though ie stuct{u32, []u8}. Nice for function return types. +1

@metaleap
Copy link
Contributor

metaleap commented Jan 31, 2020

@Rocknest from my experience in langs that have them (Haskell, C#), unnamed-field "tuples" rarely make it into a somewhat-matured final "production-grade" code product to any great extent. But IMHO they're invaluable just for more-rapidly-iterating alone already, as you restructure fn signatures quickly and re-mold your data/flow structurings at an ideally fast pace. That alone is a valid programmer use-case, making the facility fit for every programming language =)

Commonly seen is also the desire for what's hi-falutingly described as "heterogenous look-and-feel-like arrays" say Vec3[0] through Vec3[2] get accepted by compiler / type-checker, turning into field-accesses, even going as far as allowing loops. I won't argue with that, though in my practice it doesn't really come up.

@fengb
Copy link
Contributor Author

fengb commented Jan 31, 2020

@Rocknest here's a real-ish example that I've encountered building my wasm interpreter:

var stack: [65536]u8;

pub fn i32Store(self: *core.Instance, pop: packed tuple{ u32, i32 }) void {}
pub fn i64Store(self: *core.Instance, pop: packed tuple{ u32, i64 }) void {}

A packed tuple is able to represent this data directly mapped inside of the memory space. Without packing, I'd have to copy the data out (which is fine but a little inconvenient), and without tuples I'd have to emulate it with struct fields (which is fine but I'd have to invent some incrementing name and have adhoc tuples).

We already have tuples in the language because it was needed to support varargs. This proposal simply gives it a name and a way to declare the type.

@Rocknest
Copy link
Contributor

pub fn i32Store(self: *core.Instance, pop: packed tuple{ u32, i32 }) void {}

And here i am reading this code having no idea what first and second integers represent. Are you sure they do not need names?

@fengb
Copy link
Contributor Author

fengb commented Jan 31, 2020

The shared invocation context only knows the position and types. Inside the function, we can apply conceptual names, but exposing it means I cannot share the parsing logic (which is based solely in bytes).

Although thinking about it, I can possibly rely on a packed struct and iterating through the fields at comptime to generate the parser...

@ghost
Copy link

ghost commented Feb 1, 2020

I feel like a built-in function would be the best way to define tuple types. Tuples are just a special case of structs after all, so special syntax might be a bit redundant.

const MyTuple = @TupleType(u32, i32);
const MyTuple2 = @TupleType(u64,u32,bool);
const MyPackedTuple = @PackedTupleType(u32,i32);

const x : MyTuple = .{32, -5};
const y : MyTuple2 = .{1,2,false};

@fengb
Copy link
Contributor Author

fengb commented Feb 5, 2020

Another crazy idea with tuples — we can sorta create generic structs at comptime:

pub fn Soa(comptime Struct: type) type {
    const fields = std.meta.fields(Struct);
    const Data = switch (fields.len) {
        1 => struct { []fields[0].field_type },
        2 => struct { []fields[0].field_type, []fields[1].field_type },
        3 => struct { []fields[0].field_type, []fields[1].field_type, []fields[2].field_type },
        else => @compileError("TODO"),
    }

    return struct {
        data: Data,
        allocator: *std.mem.Allocator,

        pub fn initCapacity(allocator: *std, cap: usize) !Self {
            var result = Self{ .data = undefined, .allocator = allocator };
            inline for (fields) |field, i| {
                // This type of generic access is currently inaccessible
                // since we can't generate field names
                self.data[i] = try allocator.alloc(field.field_type, cap);
                errdefer allocator.free(self.data[i]);
            }
            return result;
        }
    }

@Rocknest
Copy link
Contributor

Rocknest commented Feb 5, 2020

@fengb is this what you are trying to achieve?
https://gist.github.com/Rocknest/6d07cec388036fcdf07fb4760cb92466

@frmdstryr
Copy link
Contributor

I like the separate keyword because it makes the intent more clear but am fine with either.

@BarabasGitHub
Copy link
Contributor

I've never seen really good use cases for tuples to be honest. In C++ they're super confusing, especially the way they're used in the standard library. In Python it's always tricky because you have to remember what goes where as it doesn't check anything for you. And then you want to print it because you have a bug somewhere and you're better of using namedtuple (which is like a struct, except you can't change the values once assigned).
I'm not opposed to it, but I do doubt it's usefulness.

As for multiple return values. I'm also not too fond to use tuples for that. We don't have tuples for multiple input values either, do we? If Zig wants multiple return values (and I do think that could be useful) then imho it would be better to name them (maybe name all return values?) and somehow refer to them inside the function.

@fengb
Copy link
Contributor Author

fengb commented Feb 20, 2020

Tuples were already added to replace vararg inputs. This proposal only exposes the ability to name the type so you can declare constrained tuples instead of being always generic.

@Rocknest
Copy link
Contributor

But zig's 'tuples' are really just annonymous structs. This proposal goes in somewhat opposite direction.

@fengb fengb mentioned this issue Mar 2, 2020
@andrewrk andrewrk added the contributor friendly This issue is limited in scope and/or knowledge of Zig internals. label Mar 4, 2020
@andrewrk andrewrk modified the milestones: 0.6.0, 0.7.0 Mar 12, 2020
@ominitay
Copy link
Contributor

An unintended consequence of the currently accepted syntax is that it creates an element of ambiguity. struct {} could represent a tuple type, or it could represent a struct type. Either way, we would have to decide on one or the other, which would mean that the proposed syntax wouldn't be able to fully represent all possible types. For this reason, I think that the accepted syntax should be reverted to the original tuple {} syntax.

cc: @fengb @andrewrk

@ifreund
Copy link
Member

ifreund commented Apr 18, 2022

@ominitay there is no ambiguity in the sense of ambiguity in a formal grammar. In fact, the formal zig grammar here already accepts both struct { i32, Foo } and struct { a: i32, b: Foo }. https://github.com/ziglang/zig-spec/blob/master/grammar/grammar.y

@ominitay
Copy link
Contributor

Yes, but then what type does struct {} represent? Is it a tuple or a struct? You haven't addressed what I'm saying at all. @ifreund

@ominitay
Copy link
Contributor

ominitay commented Apr 18, 2022

Again, I'm not saying that the formal grammar is ambiguous. I'm saying that what struct {} actually means hasn't been considered, and means that the accepted syntax can't actually express either an empty struct or an empty tuple.

Edit: amend typo

@ifreund
Copy link
Member

ifreund commented Apr 18, 2022

That's not a grammar ambiguity, it's orthogonal to the grammar. If you're curious what an ambiguity in the grammar would look like see this page: https://en.wikipedia.org/wiki/Ambiguous_grammar. Note that a Parsing Expression Grammar like Zig's here is by definition unambiguous: https://github.com/ziglang/zig-spec/blob/master/grammar/grammar.y.

What you are talking about is ambiguity later in the compiler pipeline. A similar ambiguity already exists with regards to the type of .{} which can be an array, tuple, or struct depending on the context.

My take is that it doesn't really matter whether @typeInfo() says struct {} is a tuple or not as long as it's well defined and consistent.

@ominitay
Copy link
Contributor

That's not a grammar ambiguity

Oop, I missed the very important word "not" between the words "I'm" and "saying" in the first sentence, completely changing the meaning. I'm completely agreed with you in that regard.

What you are talking about is ambiguity later in the compiler pipeline.

Sure, that is why I'm asking for clarification here. If we are sticking with the shared keyword approach, then we need to decide whether struct {} is a tuple or struct. I guess that for it to be a tuple would make sense, considering that the type of .{} is a tuple.

@foobles
Copy link
Contributor

foobles commented Apr 18, 2022

I agree that tuples just being a special case of structs, and struct{} vacuously being a tuple in addition to being a struct makes the most sense.

@ominitay
Copy link
Contributor

I agree that tuples just being a special case of structs, and struct{} vacuously being a tuple in addition to being a struct makes the most sense.

You can't have it be two types at once. Empty structs don't coerce to empty tuples either; this might be a non-issue, but if it is a problem, then this may need to be implemented.

@leecannon
Copy link
Contributor

Tuples already are structs, they just have a true bool in @TypeInfo instead of a false one.
Due to extra capabilities of tuples (concatenation, looping over, etc) it would make sense that struct{} would have the tuple boolean set to true.

@ominitay
Copy link
Contributor

Tuples already are structs, they just have a true bool in @TypeInfo instead of a false one.
Due to extra capabilities of tuples (concatenation, looping over, etc) it would make sense that struct{} would have the tuple boolean set to true.

We are in agreement on this :)

@Vexu
Copy link
Member

Vexu commented Jun 3, 2022

An alternative for this which doesn't require new syntax would be to allow tuples with all elements being type type to coerce to tuple types:

test {
    const T: type = .{ type, u32 };
    const t: T = .{ bool, 2 };
}

@ominitay
Copy link
Contributor

ominitay commented Jun 4, 2022

@Vexu How would you then suggest we express a packed or extern tuple type then? And how would we express field alignment?

@Vexu
Copy link
Member

Vexu commented Jun 4, 2022

There is @Type (or @Struct/#10710) and #8643. In the common case tuple fields aren't going to be aligned and at least currently packed/extern tuples do not exist in stage2.

@ominitay
Copy link
Contributor

ominitay commented Jun 4, 2022

If you think that @Tuple is enough for this, then what do you think is the point of this feature at all? Edge cases matter, we should have a broadly applicable solution. Currently packed/extern tuples doesn't exist in stage2 yes. Why does that matter at all to this discussion? It has already been agreed that they will do.

@Vexu
Copy link
Member

Vexu commented Jun 4, 2022

If you think that @Tuple is enough for this, then what do you think is the point of this feature at all?

That was a typo, I meant using @Type with is_tuple = true for making more complex tuples.

Edge cases matter, we should have a broadly applicable solution.

We already have multiple ways to create types, why does this new thing need to be able to all the same things as @Type especially as they usually won't be needed.

Currently packed/extern tuples doesn't exist in stage2 yes. Why does that matter at all to this discussion?

You mentioned it.

It has already been agreed that they will do.

I didn't know that, when did that happen?

@ominitay
Copy link
Contributor

ominitay commented Jun 4, 2022

That was a typo, I meant using @Type with is_tuple = true for making more complex tuples.
We already have multiple ways to create types, why does this new thing need to be able to all the same things as @Type especially as they usually won't be needed.

We can already everything your proposal can through @Type, so by that logic we should reject this proposal and continue with the status quo.

I forgot to mention that your solution would make it impossible for tuples to be namespaces, which is a feature that Andrew has agreed is intended, and makes sense considering enums and structs can.

Additionally, I don't see your proposal fitting in with existing precedent. Tuples are effectively structs, so why should they be declared any differently? To me, it doesn't really seem like it fits in with the language as it is today.

You mentioned it.

My point here was that this is a proposal — we are considering things that don't exist.

I didn't know that, when did that happen?

I remember Andrew talking about it on Discord at some point, and @fengb did include them as part of the proposal.

@nektro
Copy link
Contributor

nektro commented Dec 1, 2022

always could using std.meta.Tuple. this proposal rather simplified and unified their construction

@Karmylr
Copy link

Karmylr commented Dec 1, 2022

Another usecase for declared tuples: emulating multiple returns.

After #4335, Can users use tuple to return multiple values from a single function now?

@wooster0
Copy link
Contributor

wooster0 commented Dec 1, 2022

After #4335, Can users use tuple to return multiple values from a single function now?

This has always been possible simply by returning a struct from a function. This isn't much different now except you no longer have to specify field names:

const std = @import("std");

fn x() struct { u8, u16 } {
    return .{ 5, 10 };
}

pub fn main() void {
    const thing = x();
    std.debug.print("{}, {}\n", .{thing.@"0", thing.@"1"});
}

I wouldn't always recommend it as it can decrease readability (what is thing.@"0"?). Tuples definitely have its use cases though.

Edit: there are a lot of people subscribed to this issue so it may be best to continue this discussion elsewhere.

@Karmylr
Copy link

Karmylr commented Dec 1, 2022

After #4335, Can users use tuple to return multiple values from a single function now?

This has always been possible simply by returning a struct from a function. This isn't much different now except you no longer have to specify field names:

const std = @import("std");

fn x() struct { u8, u16 } {
    return .{ 5, 10 };
}

pub fn main() void {
    const thing = x();
    std.debug.print("{}, {}\n", .{thing.@"0", thing.@"1"});
}

I wouldn't always recommend it as it can decrease readability (what is thing.@"0"?). Tuples definitely have its use cases though.

Is the performance overhead of doing this bigger than using tuple?

@InKryption
Copy link
Contributor

I wouldn't always recommend it as it can decrease readability (what is thing.@"0"?). Tuples definitely have its use cases though.

Just to note, this can also be written as thing[0], as tuples are indexable, it just happens that they are also structs whose field names are their corresponding indexes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted This proposal is planned. contributor friendly This issue is limited in scope and/or knowledge of Zig internals. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

Successfully merging a pull request may close this issue.