I’ve seen similar questions to this one more than once regarding the .{} assignment syntax.
I personally rarely write code like this, so my stance on this question fluctuates… Sometimes I think the current semantics seem quite natural if I accept the RLS. But other times, I feel like it’s understandable to be bothered by the problem here, and that .{} encapsulation is more intuitive.
RLS is a feature I quite like and I have always hoped that it would be more powerful, so when this proposal was blocked by the .{} problem, I was a little sad and hoped that this problem could be solved sooner.
@mlugg 's reply on this issue has me a bit worried. What does the plan to “removing result pointers from the language” mean? How much will the RLS be affected? Is it just .{} that is affected?
Here are some personal thoughts on the current Struct Literal assignment. When we use Struct Literal assignment, what kind of behavior do we expect?
Consider a easy struct:
const S: type = struct {
a: u8,
b: u8 = 0,
c: struct {
a: u8,
b: u8,
},
d: u8,
};
If we perform the following assignment:
var s: S = .{ .a = 0, .b = 1, .c = .{ .a = 2, .b = 3 }, .d = 4 };
s = .{
.d = s.a + 1,
.a = s.b + 1,
.c = .{ .a = s.b + 1, .b = s.a + 1 },
};
try std.testing.expectEqual(@as(S, .{ .a = 2, .b = 0, .c = .{ .a = 2, .b = 3 }, .d = 1 }), s);
Semantically, this is equivalent to doing the following:
s = undefined;
s.d = s.a + 1;
s.a = s.b + 1;
s.c = undefined;
s.c.a = s.b + 1;
s.c.b = s.a + 1;
s.b = 0;
It might not be so counterintuitive.
It’s just that I may never use such semantics to write code, because for me I may prefer to use multiple statements like s.d = s.a + 1, which is more in line with the way I think when I have this kind of logic.
In addition, this semantics is too similar to the control flow in a block, and the .b that is assigned at the end always makes me feel like an implicit control flow.
The current semantics of T{} will result in another result:
var s: S = .{ .a = 0, .b = 1, .c = .{ .a = 2, .b = 3 }, .d = 4 };
s = S{
.d = s.a + 1,
.a = s.b + 1,
.c = .{ .a = s.b + 1, .b = s.a + 1 },
};
try std.testing.expectEqual(@as(S, .{ .a = 2, .b = 0, .c = .{ .a = 2, .b = 1 }, .d = 1 }), s);
Since it doesn’t use RLS, its semantics are as follows:
var expiring_s: S = undefined;
expiring_s.d = s.a + 1;
expiring_s.a = s.b + 1;
expiring_s.c = undefined;
expiring_s.c.a = s.b + 1;
expiring_s.c.b = s.a + 1.
expiring_s.b = 0;
s = expiring_s;
This semantics doesn’t use RLS, T{} shows encapsulation, so the result is consistent with many people’s intuition.
Although I am not used to using this semantics, it does save an intermediate variable and is indeed valuable. And for users who are used to Python, this usage is more familiar.
However, without RLS, this semantics ultimately requires a structure copy. If the structure is large, this semantics has a certain cost (although it can be optimized away by RVO).
My thought is: when we expect this kind of result, don’t we really want RLS?
When we expect encapsulation semantics for initializers, our expectations might be something like this:
const expiring_d = s.a + 1;
const expiring_a = s.b + 1;
const expriing_c_a = s.b + 1;
const expiring_c_b = s.a + 1;
s = undefined;
s.a = expiring_a;
s.b = 0;
s.c = undefined;
s.c.a = expriing_c_a ;
s.c.b = expiring_c_b ;
s.d = expiring_d ;
The “encapsulation” we want may just be an inline initialization function:
test "inline function init" {
const S: type = struct {
a: u8,
b: u8 = 0,
c: struct {
a: u8,
b: u8,
},
d: u8,
pub inline fn init(a: u8, maybe_b: union(enum) { manual: u8, auto: void }, c_a: u8, c_b: u8, d: u8) @This() {
const b: u8 = switch (maybe_b) {
.manual => |b| b,
.auto => 0,
};
return .{ .a = a, .b = b, .c = .{ .a = c_a, .b = c_b }, .d = d };
}
};
var s: S = .{ .a = 0, .b = 1, .c = .{ .a = 2, .b = 3 }, .d = 4 };
s = .init(s.b + 1, .auto, s.b + 1, s.a + 1, s.a + 1);
try std.testing.expectEqual(@as(S, .{ .a = 2, .b = 0, .c = .{ .a = 2, .b = 1 }, .d = 1 }), s);
}
Expanding the expressions in the Anonymous Struct Literal from left to right, and then initializes at the result location. Is this the semantics we really want? What do you think about this?