Store data in enum value?

Hi everybody,

I just started to learn Zig and try to write a little learning project: a simple CLI arc parsing library (I know there are already plenty out there).

Coming from Rust I wanted to use an enum and store data in every enum value to directly access the particular data when matching on the enum value with a switch statement. In Rust would be like that:

enum Values {Val1(String), Val2(u16)}

What’s the best way in Zig to accomplish that? Maybe something as enumArray or EnumMap or is there no equivalent in std?

I’m sorry if the answer is very obvious, but I’m still at the beginning of the learning process overlooking something is not unlikely :grin:

Thanks already in advance and happy new year!

1 Like

You can use a tagged union.

1 Like

You want a tagged union union(enum), this is what rust enums are.
I dislike that rust uses a less precise (when referring to this) term that is usually something else in other languages.

const Values = union(enum) {
 //snake case as per the style guide
 val1: ArrayList(u8), // zig doenst have special string types, only bytes
 val2: u16,
};

//...
switch (value) {
 .val1 => |string| //... ,
 .val2 => |n| //... ,
}

union(enum) is a shortcut for union(E) where E is an enum that has identical field names to the union. The latter is useful when want to use an preexisting enum for your tag.

the tagged union values also coerce to the tag values, meaning you can union_value == .enum_tag if you want.

5 Likes

Thanks for the fast response. That looks exactly like the thing I’m looking for!

I mark this as answer since its more detailed. However, @rpkak answer is also correct. Thus, thank you both!

2 Likes

While the main question is solved and marked as such, an additional one arose for me:

Is it possible to return a tagged union value from a function?

E.g. like

fn retUnion() !UnionEnum { return UnionVal("content string etc") }

Edit: Can’t format it well writing on my smartphone…

ofc it is, you can return any value from a function, as long as it matches or can coerce to the return type, even types as they are values (that’s how generics work in zig)

I think the issue is I didn’t tell you how to create a value of a union (tagged or not).

It’s just U{ .tag = val } you can replace U with . to infer the type.

With your example the ! on the return type is unnecessary, that is akin to Result<UnionEnum, Error> error is inferred, but can be specified explicitly, also zig errors can’t have payloads, they are a special type that is essentially a special enum.

An important thing to know is u.tag asserts that u is currently tag, you can u.tag = val to set its inner value. But it does not change the tag. If you want to change the tag, you instead do u = .{ .tag2 = val }.

7 Likes

OK, that’s great. Thank you very much.

Zig seems to be much more flexible than Rust in such cases. While it’s still a very early impression, I already like it very much. And it feels somehow much more natural than writing code in Rust which can be really annoying sometimes.

Good start into 26 BTW

5 Likes

I’d venture that what you’re observing is, not flexibility, but precision. In Zig, a union type, its tag, and an enum type, are three clean different things[1]. In Rust they’re conflated, sort of mushed together.

There are reasons for that, but I strongly prefer Zig’s approach to the subject. Tagged unions and enums are syntactically unified by allowing a union to be referred to as its enum in comparisons and switch statements, which is brilliant.

I wanted to add that, if a given variant of the union is void typed, you can just say

u = .void_tag;

Nor do you have to specify void, it’s inferred for you if you leave out the type:

const Jack = union(enum(u2)) {
    fee: usize,
    fie: f32,
    foe, // void
    fum: GiantKind,

    pub fn newFoe() Jack {
        // Casts to Jack
        return .foe; 
        // Equivalent:
        // return .{ .foe = {}};
    }
};

100%. Every time I write Rust these days, or even read it, I’m bowled over by how much Rust there is. It’s a lot of language!

Happy New Year! :tada:


  1. The tag is precisely and simply an enum type, what I mean is you can have enums without unions, unions without enums, and union(SomeEnum). ↩︎

12 Likes

Hey, thanks for the detailed additions. They’re very helpful.

And yes, its the precision of Zig which stands out in this point. But I can imagine that when you’re able to handle some cases (like tagged unions) more precisely, you might also get more flexibility out of that.

Plus, I totally agree on the verbosity of Rust. Its a whole lot code to write sometimes. That seems to be much more efficient in Zig. I’m curious what to discover next :grin:

1 Like

Ha! When a name is self-prophesy! “Wow… that’s a lot of rust. I didn’t expect that to happen!”

When criticizing other languages, I’d appreciate it if people could at least describe the trade-off (pros and cons). Otherwise it’s just bashing and has no value IMO.

3 Likes
  • Pros:
    • Rust has a lot of features
  • Cons:
    • Rust has a lot of features
6 Likes

My apologies. It’s was just a funny knee-jerk. I like Rust, truly.

isn’t it your lucky day

It does get macroed away at the end, but that’s provided by a 3rd party library, good luck finding a macro for every other similar situation.

I’m sure you can see the pros and cons to this.

5 Likes

That’s an excellent video, thanks for linking to it.

1 Like

Damn, what a journey that was

1 Like

:slight_smile: It’s a good video. And I know from personal experience that many of those issues are very real and very frustrating. The problems with async in particular really need to be fixed. The only good thing is that they are very well documented and considered a top priority for the Rust team. But even so it is taking a very long time for them to be addressed. So far there have only been partial improvements. We all have to choose where to put our faith when it comes to unfinished parts of a language. I think the async problems will be fixed eventually in Rust, but not as soon as anyone would like.

Exactly! That answers it succinctly for some issues. For me there were more important issues:

  • Rust doesn’t allow me to do exactly what I would like to do with memory, and I’m not willing to compromise my design. I implemented my database cache (in-memory btree) with Rust and finally realized the language was working against my design in such a big way that it just wasn’t the right tool. To make the code truly safe according to Rust’s memory model, and not compromise my design, would have required a large amount of unsafe code using raw pointers, which is very un-ergonomic and difficult to do correctly.
  • The frustrating thing is that the memory safety I was getting from Rust in this particular case wasn’t something I needed. All the data in my cache is in byte arrays, and these are reinterpreted using safe plain-old-data transformations. In Rust’s memory model, the existence of two mutable references to the same location is immediate UB; period. While in Zig this is allowed and can cause data races, but not UB. I can live with a bug causing data races due in the plain-old-data that is being cached.

If it weren’t for this issue I think Rust would have worked out fine for my use case. Of course, I get many other benefits from Zig for my use case. But in general this is a trade-off. I was also getting some nice benefits from Rust in terms of dev tooling, doc, and generics.

On Rust’s complexity issues in general I had found ways to avoid it, mostly by keeping my abstractions very simple. Creating unnecessary abstractions is a common temptation in all languages and only by lots of trial and error have I learned to (mostly) avoid it. For the async problems, however, the complexity is not currently avoidable.

4 Likes