-
-
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 T{} syntax in favor of type coercion #5038
Comments
What problem does this solve?
I'm not convinced reducing the number of ways to define an assignment is worth removing the simplest solution to the problem, if I understand this correctly. |
How about this?
|
// I prefer this
const a: [_]u8 = .{1, 2, 3}; |
I would definitely be in favor of this, just as long as the |
One could remove quoted string literal syntax since array literals could be used instead, or require all integer literals to be encoded in
and
And if there are any cases where Speaking of which,
doesn't work because the compiler infers the type that isn't legal here (the error union), rather than the one that is (Foo), so with this proposal one must do either [first way to do it]
or [second way to do it]
Casts are to be avoided, temporary variables shouldn't be necessary, type inference is nice in some places but can be hard to read and understand in others. |
@theInkSquid
It's not in the works ... #5039 was closed two days before your comment. |
@jibal Both proposals are back on the table and I'm excited/hopeful for both again. :) |
I think it should stay because of:
I also do not see what we would gain from removing it. |
This will make usage of Example: pub const Vec3 = @Vector(3, f32);
pub fn dot(a: anytype, b: @TypeOf(a)) std.meta.Child(@TypeOf(a)) {
return @reduce(.Add, a*b);
}
test {
_ = dot(@as(Vec3, .{1, 2, 3}), .{3, 2, 1}); // Looks ugly
const a: Vec3 = .{1, 2, 3}; // Vec3{1, 2, 3} looks better
const b: Vec3 = .{3, 2, 1};
_ = dot(a, b);
} With the change it will be always more convenient to use type arguments: pub fn dot(comptime T: type, a: T, b: T) std.meta.Child(T) {
return @reduce(.Add, a*b);
}
test {
_ = dot(Vec3, .{1, 2, 3}, .{3, 2, 1}); // Looks OK?
const a: Vec3 = .{1, 2, 3};
const b: Vec3 = .{3, 2, 1};
_ = dot(Vec3, a, b); // Vec3 here is redundant
} |
Another problem is a tuple with typed elements: const t = .{ .a = A{}, .b = B{} }; // works now, but will not work
const t = .{ .a: A = .{}, .b: B = .{} }; // syntax is not supported
// that's how it will look
const t1 = .{ .a = @as(A, .{}), .b = @as(B, .{}) };
const t2 = .{ @as(A, .{}), @as(B, .{}) }; Maybe in some cases the tuple with tuple elements (just |
Here's an idea of how to handle inferred-size array literals. Currently, we have a special case to allow /// This expression is the initialization expression of a var decl whose type is an inferred-length array.
/// Every result sub-expression must use array initialization syntax. The array's length should be written
/// to `chosen_len` so the caller can retroactively set the array length.
inferred_len_array_ptr: struct {
/// The array pointer to store results into.
ptr: PtrResultLoc,
/// This is initially `null`, and is set when an expression consumes this result location.
/// If an expression has a length which does not match the currently-set one, it can use `src_node` to emit an error.
chosen_len: *?struct {
len: u32,
src_node: Ast.Node.Index,
},
}, The idea here is that every peer here must be an array initialization expression ( This result location type will trigger an error in all cases other than array initializers, such as struct inits and calls through to In practice, here's what this means: // these are all valid
const x: [_]u8 = .{ 1, 2, 3 };
const y: [_]u8 = if (condition) .{ 1, 2 } else switch (x) {
.foo => .{ 3, 4 },
.bar => .{ 5, 6 },
else => .{ 7, 8 },
};
const z: [_][]const u8 = blk: {
if (foo) break :blk .{ "hello", "world" };
break :blk .{ "foo", "bar" };
};
// this is invalid
// error: array length cannot be determined
// note: result must be array initialization expression
const a: [_]u8 = @as([3]u8, .{ 1, 2, 3 });
const b: [_]i16 = blk: {
const result: [2]i16 = .{ 1, 2 };
break :blk result;
};
const c: [_]u8 = if (cond) .{ 1, 2 } else something_else;
// this is also invalid
// error: array length '3' does not match array length '2'
// note: array with length '2' here
// note: inferred-length array must have a fixed length
const d: [_]u8 = if (cond) .{ 1, 2 } else .{ 3, 4, 5 }; |
cpp2 uses the following syntax:
and allows you to omit at most one:
so if
|
If you were to follow up on this, and require the type annotation, how about functions like const a = [0]T{};
const a = std.ArrayList(T).init(std.heap.GeneralPurposeAllocator); If you want to try to remove the const a: [0]T = .{};
const a = std.ArrayList(T).init(std.heap.GeneralPurposeAllocator); To me, this just feels wrong, as both functions don't feel like they have the same “way” of writing them. I feel like if you want to remove the type annotation from the right, then you should maybe try to remove it from all functions like |
@arthurmelton See #9938 (decl literals), which would enable const a: [0]T = .{};
const a: std.ArrayList(T) = .init(allocator); |
I'd like to make one more argument against Nowadays, the only place I ever really write The problem here is that this is actually kind of type-unsafe. What would happen if I instead wrote Any given API generally expects a typed struct or an anonymous struct, rather than accepting either. Passing a typed struct where an anonymous struct is expected, or vice versa, is likely to lead to bugs. At first glance, it may seem that if anything, this is an argument in favor of Now, suppose that this proposal was implemented, so that |
I assume this addExtra api is using You can that for example in the following code: const S2 = struct{
x: i32,
y: i32,
};
fn what_is(x: anytype) void
{
// do stuff here
_ = x;
}
fn any_test(i: i32) void
{
const s1 = S2{.x = i+0, .y = i+1};
const s2 = .{.x = i+2, .y = i+3};
what_is(s1); // 1
what_is(s2); // 2
what_is(S2{.x=i+4, .y=i+5}); // 3
what_is(.{.x=i+6, .y=i+7}); // 4
what_is(.{.y=i+8, .x=i+9}); // 5
} The calls 1 and 3 are the same, the calls 2 and 4 are the same and call 5 is different. So the difference will still be there, the only thing that will probably change is that people are more likely to either use @as or just have their code accidentally do the wrong thing because they are playing around with anonymous structs instead of a real typed struct. Removing const S1 = struct{
x: i32,
y: i32,
};
const S2 = struct{
x: i32,
y: i32,
};
fn abcdef(p1: S1, p2: S2) void
{
// do stuff here
_ = p1;
_ = p2;
}
// Today
fn ghi() void
{
const s1 = S1{.x = 1, .y = 2}; // const s1: S1 = .{.x = 1, .y = 2};
const s2: S2 = .{.x = 3, .y = 4}; // const s2 = S2{.x = 3, .y = 4};
// Will silently compile parameter type change | 1 | 2 |
abcdef(S1{.x = 1, .y = 2}, S2{.x = 3, .y = 4}); // | N | N |
abcdef(.{.x = 1, .y = 2}, .{.x = 3, .y = 4}); // | Y | Y |
abcdef(.{.x = 1, .y = 2}, S2{.x = 3, .y = 4}); // | Y | N |
abcdef(S1{.x = 1, .y = 2}, .{.x = 3, .y = 4}); // | N | Y |
abcdef(s1, S2{.x = 3, .y = 4}); // | N | N |
abcdef(s1, .{.x = 3, .y = 4}); // | N | Y |
abcdef(S1{.x = 1, .y = 2}, s2); // | N | N |
abcdef(.{.x = 1, .y = 2}, s2); // | Y | N |
abcdef(s1, s2); // | N | N |
}
// Tomorrow
fn jkl() void
{
const s1: S1 = .{.x = 1, .y = 2};
const s2: S2 = .{.x = 1, .y = 2};
// Will silently compile parameter type change | 1 | 2 |
abcdef(.{.x = 1, .y = 2}, .{.x = 3, .y = 4}); // | Y | Y |
abcdef(s1, .{.x = 3, .y = 4}); // | N | Y |
abcdef(.{.x = 1, .y = 2}, s2); // | Y | N |
abcdef(s1, s2); // | N | N |
} or you would have to start littering your code with @as calls everywhere everytime but that doesn't improve readability at all but does make your code "correct". So imo your example is not really a valid argument against removing |
If I propose incorporating const a: i32 = 1;
const b = i32.{1};
const c: [3]i32 = .{1, 2, 3};
const d = [3]i32.{1, 2, 3}; // or [_]i32.{1, 2, 3};
const e: Vec2 = .{.x=1, .y=2};
const f = Vec2.{.x=1, .y=2};
const NativeFloat = if (8==@sizeOf(usize)) f64 else f32;
const g = NativeFloat.{0.25}; The three disparate syntax |
I would advise making this suggestion into its own separate proposal to potentially expedite its acceptance. Regardless of whether the parent proposal will be accepted or not, I think moving the inferred array notation to type annotations is a significant improvement in its own right. |
After originally being very opposed to the proposal I've come to like it quite a bit... Here is my argument in favour of this proposal, that I haven't seen mentioned so far: Though using type coercion for aggregate types is quite alien at first (especially coming from the many languages that require the programmer to mention a type's name in order to create a value thereof), if you think about it for some time, you'll see that we are already using this coercion system quite a bit: integer literals have type float literals have type string literals have types
|
I would like to point out that removing T{} would completely bust the barely working autocompletion. Cureently if ZLS is confused T{} is always an option to help it produce field completions. Leaving only .{} at this stage of ZLS would make dev experience much worse. |
Deficits in the functionality of third-party projects do not impact the design of Zig. A project like ZLS could handle inferred types / RLS properly with very little effort if it were appropriately designed for it. |
The following are semantically equivalent:
I propose to remove the first possibility, and rely on the other two.
The problem with this proposal as I can see it is inferred-size array literals:
There's no other way to write this. And if that works, then why wouldn't this work?
But now we're back to the proposal:
The text was updated successfully, but these errors were encountered: