While it’s certainly possible to wrap integer types using an enum or a standard struct, these approaches tend to introduce some syntactic noise in practice. Unlike primitive types, they require extra steps—such as explicitly accessing inner fields or using built-ins like @enumFromInt—which breaks the flow of usage.
This might be a minor inconvenience for the library author, but it can become quite cumbersome for downstream users.
For instance, when wrapping a slice with a regular struct, users are forced to perform additional field access every time they want to use the underlying data.
Well, if you think about it, downstream users are using Zig. They probably know the language and standard library don’t sugar much for them. But who knows what sugar (if any) we’ll get down the road? I’ve always wanted something akin to extension methods from C#, but I’ve been learning that de-sugared and transparent is often better than feels good and hides control. With the casting builtins, the intent is to prod you and ask, “How do you wanna cast this thing? What if you don’t have an enum defined for this?” Would be cool if nonexhaustive enums could coerce to their integer type and vice versa, I’ll admit. But the benefit is naming a specific type for a specific context.
I really don’t see the issue here. Is it really that much worse to type str than str.s? It’s mildly inconvenient at worst, but is that really a problem?
I find the single-member struct solution completely adequate. I use it in my own program and find the friction to be basically nonexistent. Adding a .s is not a big deal in the grand scheme of writing a program.
The problem with this is that it undoes the reason for having the enum in the first place. One of the key advantages it that you can’t mix which integers are used where. The point is that they can only be used in the right places. Usually this should be behind an api boundary where only the library author has to cast between the enum and the integer. The user just knows that they have UserId and it is incompatible with OtherId.
This applies not just to field access, but also to initialization, which currently requires curly braces {}. While this might be negligible for initializing a single object, it becomes cumbersome when dealing with large types, as the code gets cluttered with excessive braces. This lack of conciseness and cleanliness is particularly noticeable in declarative APIs.
When I’m writing code for myself, this approach feels perfectly fine. However, when I expose these types as part of a public library interface, it just feel a bit unnatural.
I see your point. I guess I would need to understand why you need each of the sub-structs to have their own separate fields. For example, if you want defaults for all of them, you can do this:
I guess I personally haven’t found the extra braces as clutter. And where possible, I have defaults so I am only forced to add the values that actually matter.
Granted, I don’t really publish libraries, but I still fail to see the problem. For use in Zig, I feel it’s perfectly fine to do this.
Perhaps if it had C bindings that would be kind of meh, but at that point I would just expose the underlying type and accompany it with initializer macros, typedefs and whatnot.
Currently, for wrapping integers, there are two methods: enum{_} and packed struct, which makes me feel that it violates the principle of ‘there should only be one obvious way to do things’.
When adding a declaration for the type constructed for @Union(...), I currently use a struct to wrap it, which I don’t really like either.
However, many implementation details are still unclear. For example, can this type use its operators and declarations in the same way as the underlying type? When wrapping an integer, if I want this design to be usable, then the value of this type should be able to directly use the operators and functions of its underlying type, right? Then, if it is a wrapped struct, is it possible to directly access the declaration of the wrapped type based on this type? Can a type that has already been wrapped be wrapped multiple times? There can be various opinions on these matters depending on how conservative one is.
Being able to directly use declarations within the namespace of the wrapped struct is likely a bad idea, because this means that once a declaration is added to the namespace of the wrapped struct, it will conflict with this wrapper type. However, not being able to use it directly will reduce the usefulness of this sugar.
However, for fields within an interface or class, users often need the flexibility to set custom values. Library authors also typically provide a range of predefined constants or helper methods beyond just a simple default. Relying solely on default field values in the type definition doesn’t adequately address these scenarios.
Currently, we seem to be left with two less-than-ideal options: either expose the raw type directly (which prevents accessing those predefined values or methods via dot notation), or wrap it in a new struct type (as discussed earlier, which introduces extra verbosity).
Regarding the .from approach you provided, that’s also a solid option for now. However, this could lead to the emergence of different dialects.
Regardless, I really appreciate your thoughtful response and the time you took to discuss this!
Builtins have more general abilities such as taking parameters that are sometimes required for converting between values of one type to another (e.g. what happens if a value can’t be exactly represented?). You’re inventing a new bit of syntax for a fairly limited functionality, so not a huge return for the complexity it adds to the compiler implementation.
Personally I distinguish between casts that merely change the interpretation of bits from casts (like you suggest) that convert representations. This kind of cast isn’t just reinterpreting the bits: it’s a conversion that results in different bits than the input. I don’t like how C conflates these different operations into a single syntax - it makes it less clear what is actually going on.
For now I just use little inline wrappers to contain the ugly bits I need to use frequently for the specific program. There are only a few places where I need to cross float/int boundaries. The friction has led me to rethink the way I would have done it in another language and I’m finding that it’s often a real improvement - simpler, more direct, less abstracted code that stays closer to the native data format I’m working with.