-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
remove var args and add anon list initialization syntax #208
Comments
The topic of tuples was one of the inspirations for the discussion that went into #83 (comment) . Here's what it means for tuples: Zig is resistant to the idea of having general-purpose tuples simply because they don't seem useful enough to justify including the in the language. But here's what is useful that's like tuples, and we want to include in Zig: Returning multiple things from a function at onceThis is done with named return types. See #83. Expressions initializing multiple values at onceThis is done with all expressions and return types having a number of expressions returned, possibly 0. See #83. Example: const a, const b = if (something) {
1, 2
} else {
printf("second case\n");
2, 1
}; Variadic functions (varargs)See #77. It's still to be seen how varargs will affect the existence and features of tuples. Maybe we will have tuples as a consequence of the |
Proposal: Add tuples and remove var argsVar args turned out to be rather messy and bug prone:
Further, it can be abused to achieve default function parameters (rejected in #484) and we don't want that. Even for functions such as Var args right now is essentially a special-case tuple, with a bunch of arbitrary rules and special cases. So this proposal is to add tuples to the language and remove var args. Calling pub fn main() void {
const x: i32 = 1234;
const y = "aeou";
std.debug.warn("number: {}, string: {}\n", [x, y]);
}
A tuple has comptime-known length (via the Tuple literals implicitly cast to arrays, when all of the elements of the tuple implicitly cast to the array type, and the length matches. I also propose The syntax for a tuple type is The definition of a function which accepts a tuple as an argument would likely use fn print(comptime format: []const u8, args: var) void {
inline for (args) |arg| {
// `for` works with tuples
}
} You would need the Here's an example from another issue, division that wants to report fn div(numerator: i32, denominator: i32) !tuple{i32, i23} {
if (denominator == 0) return error.DivideByZero;
// ...
return [a, b];
} One more note on the literal syntax. I have an idea for an untyped struct literal that would look like (notice the dot in front): .{.field = value, .field2 = value2} This is to match untyped enum literal syntax, useful in switch cases: .EnumValue If we kept these in mind, then it might make sense, instead of Tuple instances can be accessed with array access syntax: |
I like the |
I personally prefer the I feel like thinking of a tuple as a struct with anonymous fields makes more sense than thinking about it as an array like type. |
@alexnask -- https://en.wikipedia.org/wiki/Tuple
But I'm open to the possibility that Zig is more interested in the machine representation than in type theory. Or, maybe this isn't a tuple at all and should be conceived of in a different way entirely. |
I feel like using array literal syntax may lead people to believe the fields are contiguous in memory, while tuple will probably be equivalent to the default layout of a struct with the fields of the types. At the end of the day it's just a question of syntax :P |
What about using |
I personally prefer the ( a, b, c, ) syntax for tuples。With the square brackets like [a, b, c] would i be easy to confuse it with arrays。 |
Part of this proposal breaks the usage of var as a function param. For example this function would now be accepting a tuple and not a single argument.. pub fn abs(x: var) @typeOf(x) { .. } so maybe the type should just be tuple pub fn writeln(fmt: []u8, args: tuple) !void { ... } It also seems like there are two kinds of tuple, named and index tuples. named: [x = 1, name = "hello", pi = 3.14] The named tuples look like anonymous structs and might be better with a different syntax: fn foo(config: struct{index: i32, name: []u8, offset: u8}) struct{x: i32, y: u64} {
return @typeOf(foo).Fn.return_type{.x = 0, .y = 1};
// or
return .{.x = 0, .y = 1};
}
var point = foo( .{ .index = 1, .name = "hammer time", .offset = 0} );
point.x + point.y; I would also suggest anonymous enums and unions as well. There are a lot of possible tuple syntax options: var a: i32 = 1; |
I think I'll dig into Zig and implement tuples soon(ish). Here is how I imagine tuples working: const assert = @import("std").debug.assert;
test "Tuples!" {
// Like enums, structs and unions, tuples will have their own keyword for declaring
// tuple types.
const Tuple = tuple.{i32, []const u8};
const Struct = struct.{a: i32, b: []const u8};
const Enum = enum.{A, B};
const Union = union.{A: i32, B:[]const u8};
// Two tuples types are the same, as long as all their members are the same. This
// is different from how structs, enums and unions work,
assert(tuple.{u8} == tuple.{u8} );
assert(struct.{a:u8} != struct.{a:u8});
assert(enum.{A} != enum.{A} );
assert(union.{A:u8} != union.{A:u8} );
// Tuples use the array initializer syntax, but each element can be of different
// types. No new initializer syntax.
const t = tuple.{i32, []const u8}.{0, ""};
const a = [2]u8.{0, 1};
// To access the items of a tuple, the index operator is used.
// This allows tuples to be used with inline while, to iterate over all members.
const a = t[0]; // Here, the index have to be comptime known
// Tuples can be destructured into multible variables.
// This allows for multible return types from both functions and labled blocks:
const min, const max = blk: {
var arr = []u8{2,3,5,2};
var min = arr[0];
var max = arr[0];
for (arr[1..]) |i| {
if (i < min)
min = i;
if (max < i)
max = i;
}
break :blk tuple.{u8, u8}.{min, max};
};
assert(min == 2 and max == 5);
// We might even allow assignment into already declared variables:
var a: u8 = undefined;
a, const b = tuple.{u8, []const u8}.{0, ""};
// *** None essential features ***
// We might also allow the "++" operator on tuples:
const t1 = tuple.{u8}.{0};
const t2 = tuple.{[]const u8}.{""};
var t: tuple.{u8, []const u8} = t1 ++ t2;
}
test "Inferred initializers" {
// Inferred initializers are initializers whos types are inferred from the context
// in which they are used. They allow for shorthand initialization where it does
// not improve readablilty to append the initializer with a type.
// Inferred initializers come i 3 syntactic flavors.
// structs and unions will use "inferred struct initializers"
const s: struct.{a: i32, b: []const u8} = .{ .a = 0, .b = "" };
const u: union.{A: i32, B: []const u8} = .{ .A = 0 };
// tuples and arrays will use "inferred tuple initializers"
const t: tuple.{i32, []const u8} = .{0, ""};
const a: [2]u8 = .{0, 1};
// enums will use "inferred enum initializers"
const e: enum.{A, B} = .A;
// They will have their own internal temporary type, which implicitly cast to
// other types using the following rules:
// * "inferred struct initializers" will implicitly cast to:
// * Structs, If:
// * The initializer has the same number of fields as the struct.
// * All initializer field names are also in the struct.
// * All initializer fields implicitly casts to the struct field of the
// same name.
// * Unions, If:
// * The initializer has 1 field.
// * The initializers fields name is contained in the union.
// * The initializers field implicitly casts to the tag of that name.
// * "inferred tuple initializers" will implicitly cast to:
// * Arrays, If:
// * The initializer has the same number of items as the array.
// * All initializers items implicitly cast to the arrays item.
// * Tuples, If:
// * The initializer has the same number of items as the tuple.
// * All initializers items implicitly cast to the tuples items.
// * They will also cast to a normal tuple, if none of the above is true.
// * This is useful for passing inferred tuple initializers to functions
// taking "var".
// * "inferred enum initializers" will implicitly cast to:
// * Enum, If:
// * The initializers name is contained in the enum.
// * Union, If:
// * The initializers name is contained in the union.
// * The initializers name in the union is "void".
// In userspace, inferred initializers doesn't have a type. You therefore can't
// call @typeOf on them. They also can't be stored at runtime because of this.
// error: enum initializers doesn't have a type.
_ = @typeOf(.E);
// error: enum initializers doesn't have a type
_ = funcTakingVar(.E);
// error: .E does not have a runtime representation.
var a = .E;
// error: struct initializers doesn't have a type.
_ = @typeOf(.{ .a = 2, .b = 4 });
// error: struct initializers doesn't have a type.
_ = funcTakingVar(.{ .a = 2, .b = 4 });
// error: .{ a: u8, b: u8 } does not have a runtime representation.
var a = .{ .a = u8(2), .b = u8(4) };
// The exception here is inferred tuples, which will cast to a normal tuple when
// it is not inferred.
_ = @typeOf(.{ 2, 4 });
_ = funcTakingVar(.{ 2, 4 });
var a = .{ u8(2), u8(4) };
}
// These two features allows us to have:
// *** Varargs ***
// With these two features, we can remove varargs, and have std.debug.warn be
// used like this:
fn warn(comptime fmt: []const u8, tuple: var) void { ... };
test "warn" {
// This proposal does not solve passing comptime_int to varargs functions
// VVVVV
warn("{} {}", .{ u8(1), "yo" });
}
// This also allows us to have "typed varargs" functionality:
fn concat(args: [][]const u8) []const u8 { ... };
test "concat" {
const res = concat(.{"Hello", " ", "World!\n"});
}
// *** Named arguments ***
// related: https://github.com/ziglang/zig/issues/479
const Flags = struct {
turn_on_nuke: bool,
go_full_screen: bool,
take_over_the_univers: bool,
};
fn something(flags: Flags) void { ... }
test "named args" {
// With https://github.com/ziglang/zig/issues/485, we even get default argument
// (https://github.com/ziglang/zig/issues/484) values.
something(.{
.take_over_the_univers = true,
.turn_on_nuke = true,
.go_full_screen = false,
});
} |
Hi @Hejsil
does it mean we would be able to initialize arrays this way? const Struct = struct.{a: i32, b: []const u8};
const structs = []Struct.{
.{.a = 0, .b = "0"},
.{.a = 0, .b = "0"},
.{.a = 0, .b = "0"},
...
}; or this way? const Struct = struct.{a: i32, b: []const u8};
const structs: []Struct = .{
.{.a = 0, .b = "0"},
.{.a = 0, .b = "0"},
.{.a = 0, .b = "0"},
...
}; Or both? |
I've discussed this more with @nodefish and @tgschultz on IRC, and @nodefish pointed out the following:
Tuples go against the "Only one obvious way to do things.", and with this in mind we came up with a counter proposal, that gets us most of the same things: const assert = @import("std").debug.assert;
test "Multible return values" {
const S = struct { a: u8, b: []const u8 };
// Structs and arrays can be desconstructed into their fields/items.
const a, var b = S.{ .a = 0, .b = "" };
const a, var b = [2]u16{1, 2};
// This allows for multible return types from both functions and labled blocks:
const min, const max = blk: {
var arr = []u8{2,3,5,2};
var min = arr[0];
var max = arr[0];
for (arr[1..]) |i| {
if (i < min)
min = i;
if (max < i)
max = i;
}
break :blk []u8.{min, max};
};
assert(min == 2 and max == 5);
}
test "Inferred initializers" {
// Inferred initializers are initializers whos types are inferred from the context
// in which they are used. They allow for shorthand initialization where it does
// not improve readablilty to append the initializer with a type.
// Inferred initializers come i 3 syntactic flavors.
// structs and unions will use "inferred struct initializers"
const s: struct.{a: i32, b: []const u8} = .{ .a = 0, .b = "" };
const u: union.{A: i32, B: []const u8} = .{ .A = 0 };
// arrays will use "inferred array initializers"
const a: [2]u8 = .{0, 1};
// enums will use "inferred enum initializers"
const e: enum.{A, B} = .A;
// They will have their own internal temporary type, which implicitly cast to
// other types using the following rules:
// * "inferred struct initializers" will implicitly cast to:
// * Structs, If:
// * The initializer has the same number of fields as the struct.
// * All initializer field names are also in the struct.
// * All initializer fields implicitly casts to the struct field of the
// same name.
// * Unions, If:
// * The initializer has 1 field.
// * The initializers fields name is contained in the union.
// * The initializers field implicitly casts to the tag of that name.
// * They will also cast to a normal struct, if none of the above is true.
// * This is useful for passing inferred struct initializers to functions
// taking "var".
// * "inferred array initializers" will implicitly cast to:
// * Arrays, If:
// * The initializer has the same number of items as the array.
// * All initializers items implicitly cast to the arrays item.
// * They will also cast to a normal array, if none of the above is true.
// * This is useful for passing inferred arrays initializers to functions
// taking "var".
// * "inferred enum initializers" will implicitly cast to:
// * Enum, If:
// * The initializers name is contained in the enum.
// * Union, If:
// * The initializers name is contained in the union.
// * The initializers name in the union is "void".
// * They will also cast to a normal enum, if none of the above is true.
// * This is useful for passing inferred enums initializers to functions
// taking "var".
// Implicitly casts to enum{E}
_ = @typeOf(.E);
_ = funcTakingVar(.E);
var a = .E;
// Implicitly casts to struct.{a: u8, b: u8}
_ = @typeOf(.{ .a = u8(2), .b = u8(4) });
_ = funcTakingVar(.{ .a = u8(2), .b = u8(4) });
var a = .{ .a = u8(2), .b = u8(4) };
// Implicitly casts to [2]u16
_ = @typeOf(.{ u8(2), u16(4) });
_ = funcTakingVar(.{ u8(2), u16(4) });
var a = .{ u8(2), u16(4) };
}
// *** Varargs ***
// We can remove varargs, and have std.debug.warn be used like this:
fn warn(comptime fmt: []const u8, args: var) void { ... };
test "warn" {
// This proposal does not solve passing comptime_int to varargs functions
// VVVVV
warn("{} {}", .{ .a = u8(1), .b = "yo" });
// We can also use the names to have order independent fmt
warn("{b} {a}", .{ .a = u8(1), .b = "yo" });
// Format params will be passed after the ':'
// V V
warn("{:x} {a:x}", .{ .a = u8(1) });
// Printing structs require that it is passed inside the "fmt" struct
const S = struct.{a: u8, b: u8};
warn("{}", .{ .a = S{.a = 0, .b = 0} });
}
// This also allows us to have "typed varargs" functionality:
fn concat(args: [][]const u8) []const u8 { ... };
test "concat" {
const res = concat(.{"Hello", " ", "World!\n"});
}
// *** Named arguments ***
// related: https://github.com/ziglang/zig/issues/479
const Flags = struct {
turn_on_nuke: bool,
go_full_screen: bool,
take_over_the_univers: bool,
};
fn something(flags: Flags) void { ... }
test "named args" {
// With https://github.com/ziglang/zig/issues/485, we even get default argument
// (https://github.com/ziglang/zig/issues/484) values.
something(.{
.take_over_the_univers = true,
.turn_on_nuke = true,
.go_full_screen = false,
});
} |
@andrewrk I saw somewhere on IRC that you where considering added something like this as comptime fields: const A = struct {
a: u8,
b: u16,
comptime c: u8 = 100,
}; I had another idea i wanted to share. We could have comptime fields by adding a new type to the language. You create it with the builtin
The above example becomes: const A = struct {
a: u8,
b: u16,
c: @OnePossibleValue(u8(100)),
}; More details on how it would work:
|
@Hejsil ooh, I like this. It solves the problem with a slightly less intrusive modification to the language. Some observations:
A downside of this is that reflection code iterating over struct fields would have to explicitly be aware of this weird type. With the "comptime fields" thing, the code iterating over struct fields would be able to treat comptime fields and normal fields the same (and just happen to get comptime values sometimes when doing field access). |
<andrewrk> tomorrow, end of day, ziglang#3580 will be closed and ziglang#3575 will be closed, and dimenus's examples will work (except for vector literals which is not happening, see ziglang#208)
This implements stage1 parser support for anonymous struct literal syntax (see #685), as well as semantic analysis support for anonymous struct literals and anonymous list literals (see #208). The semantic analysis works when there is a type coercion in the result location; inferring the struct type based on the values in the literal is not implemented yet. Also remaining to do is zig fmt support for this new syntax and documentation updates.
This implements stage1 parser support for anonymous struct literal syntax (see #685), as well as semantic analysis support for anonymous struct literals and anonymous list literals (see #208). The semantic analysis works when there is a type coercion in the result location; inferring the struct type based on the values in the literal is not implemented yet. Also remaining to do is zig fmt support for this new syntax and documentation updates.
So I have some points to make in favor of variadic arguments, or should I say against the actual list initialization syntax replacing it, that is limited in some cases and causes regression in zig's expressiveness. I know I'm not coming with the most compelling examples ever, but please acknowledge that they can be real usecases, and correct me if I'm wrong or missing something ! I'll call Generic functions flaws:
Here is a sample zig program trying to illustrate these problems. const std = @import("std");
const builtin = @import("builtin");
const warn = std.debug.warn;
pub fn main() void {
{
// OK
const a = add(12, 34);
warn("add is {x}\n", add); // the function can be referenced as is
const T = @typeOf(add);
warn("add is of type {}\n\n", @typeName(T));
}
{
const b = addGeneric(u32, 12, 34); // ok
// warn("addGeneric is {x}\n", addGeneric);
// Error : argument of type '*const fn(type,var,var)var' requires comptime.
// Indeed the types of args are not resolved.
// const T = @typeOf(addGeneric ???) // impossible
// But we cannot express the instanciation of addGeneric with specific
// argument types. This kind of function is a second-class citizen.
// It cannot be referenced.
// In this case, it can be worked around like this:
const workaround = addGenericWorkaround(u32);
const c = workaround(12, 34);
warn("addGeneric(u32) is worked around by {x}\n", workaround);
const T = @typeOf(workaround);
warn("workaround is of type {}\n\n", @typeName(T));
}
{
variadicFn("foo", false); // ok
// warn("variadicFn is {x}\n", variadicFn);
// Error : argument of type '*const fn(var)var' requires comptime.
// Indeed the types of args are not resolved.
// But we cannot express the instanciation of variadicFn with specific
// argument types. This kind of function is a second-class citizen.
// It cannot be referenced.
const T = fn ([]const u8, bool) @typeOf(variadicFn("foo", false));
warn("variadicFn([]const u8, u32) is of type {}\n\n", @typeName(T));
}
{
varFn(.{ "foo", false }); // ok
// warn("varFN is {x}\n", varFn);
// Error : argument of type '*const fn(var)var' requires comptime.
// Indeed the types of args' fields are not resolved.
// But we cannot express the instanciation of varFn with specific
// argument types. This kind of function is a second-class citizen.
// It cannot be referenced.
// In this case, it can be worked around like this:
// const args = .{ "foo", false }; // assertion in the compiler, is this allowed ?
// const workaround = varFnWorkaround(@typeOf(args));
const Args = struct {
a: []const u8,
b: bool,
};
const args: Args = .{ .a = "foo", .b = false };
const workaround = varFnWorkaround(Args);
workaround(args);
warn("varFn({}) is worked around by {x}\n", @typeName(Args), workaround);
// This is not particularly elegant, to say the least.
const T = fn (Args) @typeOf(varFn(.{ "foo", false }));
warn("workaround is of type {}\n", @typeName(T));
}
}
comptime {
@export("add", add, builtin.GlobalLinkage.Strong); // ok
@export("addGenericU32", addGenericWorkaround(u32), builtin.GlobalLinkage.Strong); // ok-ish
// Now maybe I am using variadicFn/varFn for a lot of types, and some instanciations
// are candidates to being exported as a library, if they don't use exclusive zig features.
// @export("variadicFn_u32_bool", ???, builtin.GlobalLinkage.Strong); // impossible
// It's possible to declare the function as export but then you can't control
// which instanciations are exported, if you want only a subset of what exists
// in your program.
// Since actual instanciated function's arguments can be accepted by the ccc,
// it _could_ have been possible to export it.
const Args = struct {
a: u32,
b: bool,
};
// @export("varFn_u32_bool", varFn, builtin.GlobalLinkage.Strong); // impossible
// @export("varFn_u32_bool", varFnWorkaround(Args), builtin.GlobalLinkage.Strong); // also impossible
}
/// Old-style function with variadic arguments
extern fn variadicFn(args: ...) void {
// ...
}
/// New-style function with variadic arguments
fn varFn(args: var) void {
// Used like varFn(.{.foo = "foo", .bar = false });
// Or varFn(.{"foo", false });
// Number of fields in the anonymous struct/list is unknown.
// ...
}
/// New-style workaround
fn varFnWorkaround(comptime T: type) fn (T) void {
return struct {
pub fn workaround(args: T) void {
varFn(args);
}
}.workaround;
}
/// Generic function
fn addGeneric(comptime T: type, a: T, b: T) T {
return a + b;
}
/// Workaround
fn addGenericWorkaround(comptime T: type) extern fn (T, T) T {
return struct {
pub extern fn add(a: T, b: T) T {
return addGeneric(T, a, b);
}
}.add;
}
/// Usual dummy function
extern fn add(a: u32, b: u32) u32 {
return a + b;
} Even though we can construct the type of specific instanciations for each generic function, it's not a general solution and requires it to be hand-written. In the end, they are functions but with less features compared to traditional ones. Functions taking anonymous struct/list arguments also have a flaw that didn't exist before, with variadic arguments: they cannot treat the fields of the Here's a simple program demonstrating what is possible with variadic arguments, but not with anonymous struct/list currently. const std = @import("std");
const builtin = @import("builtin");
const warn = std.debug.warn;
/// Practical wrapping of function call via variadic arguments
fn time(comptime fun: var, args: ...) void {
var timer = std.time.Timer.start() catch unreachable;
fun(args);
warn("{} ns\n", timer.read());
}
/// Wrapping of function call with anonymous struct/list
fn time2(comptime fun: var, args: var) void {
var timer = std.time.Timer.start() catch unreachable;
// fun(args); // error : bad number of arguments
// Can't forward arguments to fun
// The only way to make it work is to refactor every function we want to
// call via time2 and make it use anonymous struct/list for arguments.
warn("time2 can't call {}\n", fun);
}
fn sleep() void {
std.time.sleep(88);
}
var res: u32 = undefined;
fn mul(a: u32, b: u32, c: u32) void {
res = a * b * c;
}
pub fn main() void {
warn("timing mul ");
time(mul, @as(u32, 12), @as(u32, 34), @as(u32, 56));
time2(mul, .{ @as(u32, 12), @as(u32, 34), @as(u32, 56) });
warn("timing sleep ");
time(sleep);
time2(sleep, {});
} |
The second problem could be addressed with a new builtin called |
To help show the difference between fn foo(value: var) void; A new foo(0);
foo(1); // this uses the same instantiation of foo as the previous call
foo(2); // still the same instantiation
foo("hello"); // a different instantiation of foo since it's argument is a different type
foo("there"); // same instantiation as the previous type Since every foo(@OneValueType(0).init());
foo(@OneValueType(1).init()); // this is a different instantiation of foo than the previous call
foo(@OneValueType(2).init()); // yet another instantiation
foo(@OneValueType("hello").init()); // still another instantiation
foo(@OneValueType("there").init()); // still another instantiation Note that it's a trade-off between code size and argument size. A new Also note that |
I have been reading and rereading this discussion and I cannot quite grasp what will replace var args; will there be a syntax to declare and initialize anonymous regular structs in one go? The "anonymous struct literal" feature does it, obviously, but only for literals; so the resulting struct cannot contain runtime values, correct? So is the answer that we'll be able do to something like the following?
If I missed something obvious, many thanks for pointing it out to me. |
Sorry to comment on an already closed issue, but do we have an issue discussing |
It was briefly discussed again in #3677 but since it wasn't used to implement this feature it didn't get much attention. Feel free to make a proposal for it. |
Accepted Proposal
Tuple is a powerful construct in modern language when one doesn't want to define a struct simply to return/pass grouped information. I'd like to be able to do:
Not sure if this should be part of the language or simply baked in the standard library, e.g like scala where they define
Tuple2(inline A: type , inline B: type)
. They do have sugaring that can extract in theval (x, y) = expr
construct though.The text was updated successfully, but these errors were encountered: