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: Give @as its own syntax #16397

Closed
tecanec opened this issue Jul 13, 2023 · 6 comments
Closed

Proposal: Give @as its own syntax #16397

tecanec opened this issue Jul 13, 2023 · 6 comments

Comments

@tecanec
Copy link
Contributor

tecanec commented Jul 13, 2023

This proposal is not intended to change the semantics of Zig; it is purely meant to discuss the aesthetics and readabillity of code. As such, I will discuss this as a human who just so happens to be writing a lot of Zig-code, so expect me to take a less objective approach to discussing this than I normally would.

The Zig programming language relies heavily on its extensive type system, and there are many cases where the exact type of a value is important. As such, there are many cases where the @as builtin is needed, such as for controlling integer overflows, or occasionally when calling functions or builtins with anytype parameters. Recent changes also require @as as a compliment to the likes of @intCast and @intFromFloat` when the resulting types can not be easily deduced.

Now, I know this is likely subjective, and I'm not gonna assume that the majority agrees with me, because I honestly do not know. (This proposal can be ignored otherwise.) But personally, I don't really like the syntax of the @as builtin. It frequently adds more nested brackets that I'm comfortable with, and the function call-esque syntax makes it feel more elaborate than it really is. Overall, the "mental weight" (so to speak) of this syntax feels way out of proportion to what it's actually doing, and so I tend to feel like I have to avoid it even when I really should be using it. I therefore propose that we replace the builtin with a new syntax.

Another consideration is when defining parameters for generic functions. These often raise the question of whether to use fn foo(comptime T: type, x: T) T or fn foo(x: anytype) @TypeOf(x). The implementation has to make this decision, and the application has to remember it. But really, the difference is just that the former lets the caller skip an @as.

Here are some possible syntaxes, as well as my personal opinions (feel free to discuss):

@as(T, val): The current syntax, listed for completeness's sake and as something to compare with. I've already stated my opinion, but to summarize, I find this syntax a bit overwhelming for such a semantically simple operation. Example:

const x_u32 = @as(u32, 4000);
const x_vec = @splat(4, @as(u32, 27));
const x_f32 = @as(f32, @floatFromInt(x_u32));
const x_of = @as(u16, @intCast(x_u32)) +% @as(u16, 31<<11);
const y = bar(@as(i32, -30));

(T)val: The syntax used by most languages with C-like syntax. Personally, I'm not a fan of using paranthesis in this manner, since they're also used for function calls and for overriding predecense, but it's easily recognized by non-Zig-writers. Example:

const x_u32 = (u32)4000;
const x_vec = @splat(4, (u32)27);
const x_f32 = (f32)@floatFromInt(x_u32);
const x_of = (u16)@intCast(x_u32) +% (u16)(31<<11);
const y = bar((i32)-30);

T(val): Another syntax that is also accepted by C and C++. Given Zig's treatment of types, this is grammatically the same as calling the type like a function. On one hand, this avoids complicating the grammar entirely, but on the other, it means that the same grammar has two different meanings depending on whether its given a type or a function. Example:

const x_u32 = u32(4000);
const x_vec = @splat(4, u32(27));
const x_f32 = f32(@floatFromInt(x_u32));
const x_of = u16(@intCast(x_u32)) +% u16(31<<11);
const y = bar(i32(-30));

val: T: Resembles the syntax for typing declarations, which may help with making this syntax more intuitive. It can also be parsed as a binary operation between val and T. This is my personal favorite; I like its intuitiveness and the simplicity of its syntax, and I think it looks aesthetically pleasing in many use-cases. However, care should be taken to avoid making the grammar of sentiel-terminated array types ambiguous. Example:

const x_u32 = 4000: u32;
const x_vec = @splat(4, 27: u32);
const x_f32 = @floatFromInt(x_u32): f32;
const x_of = @intCast(x_u32): u16 +% (31<<11): u16;
const y = bar(-30: i32);

There is also the matter of maintaining the "one canonical solution"-rule. Specifically, we should consider the case of const x: u8 = 5; versus const x = @as(u8, 5);. This is not a new dilemma, but giving @as a more "out-of-the-way" syntax actually serves to encourage the former. Simplifying the syntax of @as may loosen this, however. I am not sure whether or not this could be an actual problem.

@Validark
Copy link
Contributor

That last suggestion is interesting, though I'm surprised you didn't suggest an infix as operator like they have in TypeScript. Even still, reducing the number of parenthesis can improve readability, particularly when there isn't a decent name to give a constant variable. I don't know what the best answer is but I'm definitely interested in more suggestions and opinions on this issue.

I do have a question though: do you ever wish there was a better syntax for @as when dealing with non-numbers? Part of me wonders whether a better solution would be allowing numbers to be declared with a type, like 100u32 or 100_u32. Maybe that would alleviate the majority of issues?

@IntegratedQuantum
Copy link
Contributor

IntegratedQuantum commented Jul 13, 2023

do you ever wish there was a better syntax for @as when dealing with non-numbers?

I think vectors are also a good use-case. As far as I know there is plans to implement all the casting operations for vectors as well and additionally just today there was a PR that changed how @splat works. Now we need to write @as(VecType, @splat(27)) instead of coercing just the number.

Part of me wonders whether a better solution would be allowing numbers to be declared with a type, like 100u32 or 100_u32.

That would only work on number literals though.
And in my code for example most of the instances of @as occur because of casting operations.

I think it is also important to consider how this affects non-numeric types. Here is a ptrCast example from my code:

const buffer = @as([*]f32, @ptrCast(@alignCast(cVoidPtr)))[0..cPtrLen];
const buffer = (([*]f32)@ptrCast(@alignCast(cVoidPtr)))[0..cPtrLen];
const buffer = [*]f32(@ptrCast(@alignCast(cVoidPtr)))[0..cPtrLen];
const buffer = (@ptrCast(@alignCast(cVoidPtr)): [*]f32)[0..cPtrLen];

@rohlem
Copy link
Contributor

rohlem commented Jul 13, 2023

I think something that should be considered in the discussion is the ordering of elements:

// status-quo declaration:
// (name) (type) (expression)
var value: u128 = slice.len;
var value = @intFromEnum(slice[5]);
var value = @as(u128, slice.len);
var value = (u128) slice.len;
var value = u128(slice.len);
// ^ these two proposed options match in ordering

// `: type` syntax for expressions would diverge from this,
// as would an infix `as` operator (that reads naturally)
// (name) (expression) (type)
var value = (slice.len: u128);
var value = slice.len as u128;

// however, an infix `from` operator would fit in with status-quo:
// (name)  (type)   (expression)
var value = u128 from slice.len;
personal opinion

Based on my current code base I'm relatively neutral on this proposal,
but wouldn't mind a solution that is considered less noisy for affected use cases / readers.
T from x syntax is probably my favorite so far because of how close it is to
@tFromX(x) builtins
and T{...} aggregate syntax.

(Also unrelated, but if it's considered more readable in certain contexts we could add std.meta.splatN/nSplat with the signature of the old @splat.)

@jedisct1
Copy link
Contributor

I like status quo three reasons:

  1. It's greppable. Other proposals are not, at least not as easily.
  2. When used within an expression, there's no operator precedence to remember of, no confusion about what the type widening applies to.
  3. Consistency with other type conversion builtins.

@andrewrk
Copy link
Member

This proposal is motivated by a desire for type coercion to have less friction than it does currently, which is a desire shared by yours truly.

However, none of the suggestions in this thread are actually improvements, and all have been considered multiple times. In fact, Zig used to have an infix as operator. You can find this in the git commit history. Likewise, it used to have type coercion via function call syntax (#1757). The others were considered and rejected already.

@andrewrk andrewrk closed this as not planned Won't fix, can't repro, duplicate, stale Jul 13, 2023
@lerno
Copy link

lerno commented Jul 17, 2023

For the sake of completeness (in case this issue is referenced again) one should probably consider @as(i32)x (or perhaps @cast(i32)x) in addition to the previously mentioned (i32)x i32(x). This is used in D for example (although without @ prefix).

However, in that case other pointer casts should probably then need to be reworked in a similar prefix-style manner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants