5 ms·
Oh, why have they redefined the word "enum" to mean a "union"? enum Result<T, E> { Ok(T), Err(E), }
by ivoras 5y ago
Oh, why have they redefined the word "enum" to mean a "union"?
enum Result<T, E> {
Ok(T),
Err(E),
}
- steveklabnik 5y agoThey're not only unions, they're closer to discriminated unions. Rust also has unions, with the "union" keyword.
- estebank 5y agoTo extend on what Steve mentioned, `union` in Rust is a C-style union, but because the type carries no information on what variant the underlying bits represent, accessing them is `unsafe`[1]. On the other hand, `enum`s in Rust are tagged unions, they reserve some data for the typesystem to determine at runtime which variant any given instance corresponds to. Because of that you can't interpret the T as an E with an enum, but you can with an union. [1]: https://doc.rust-lang.org/reference/items/unions.html https://doc.rust-lang.org/reference/items/unions.html
- ziml77 5y agoAnd to add onto that, you can also use a Rust enum like a traditional enum: enum Foo { OptionA, OptionB, OptionC, } enum Bar { OptionA = 1, OptionB = 2, OptionC = 4, }
- scoutt 5y ago> to determine at runtime I didn't know this. How is determined? Is there any code inserted by the compiler to do this? What's the overhead of using Result<T, E>?
- estebank 5y agoTL;DR: the overhead for enums that don't have an inordinate amount of variants is about 8 bytes, but can be zero in special cases. > What's the overhead of using Result<T, E>? Enough bits (+ padding) to discriminate between all the variants, as any tagged union would, modulo some tricks for niche optimization. For example, Result<i64, u64> is 16 bytes in memory because it needs 8 bytes to represent either a i64 or an u64, plus another 8 bytes to determine whether it is an Ok or an Err. You can think of it as a struct Enum { variant: usize, bytes: [u8; N], } The niche optimization I mentioned applies for things like Option<NonZeroI64>, where NonZeroI64 fits in 8 bytes but the compiler can ensure that the bitpattern corresponding to the 0 value will never be used, so it can use that to differentiate between Some and None. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6c4de0addbabb43ff279af2b8333be56 https://play.rust-lang.org/?version=stable&mode=debug&editio...
- scoutt 5y agoThank you. Regarding size, from an embedded perspective, do you think all these enums, in a very deep call stack can, ultimately, overflow my stack? Because for every function call there should be 8, or 16 or 24 or N bytes of stack-allocated space to hold these enums. Is this something embedded Rust developers keep in mind? Or perhaps worry about the extra copies it has to make when functions return? Regarding code: because of the "to determine at runtime" of your parent comment, I thought there could be some runtime checks (extra code) for handling enums.
- estebank 5y ago> Regarding size, from an embedded perspective, do you think all these enums, in a very deep call stack can, ultimately, overflow my stack? Because for every function call there should be 8, or 16 or 24 or N bytes of stack-allocated space to hold these enums. Is this something embedded Rust developers keep in mind? Or perhaps worry about the extra copies it has to make when functions return? I don't think I have come across any conversation by embedded developers foregoing enums because of that, but anecdotally that space does seem to rely on type state machines a bit more than others, which have no runtime memory cost. A bigger cost consideration is if you have an enum with variants that have wildly different sizes, making the enum big for all cases, whether it is necessary or not. We have a lint to encourage you to Box wildly diverging variants, but that might not be possible if you don't have an allocator. > Regarding code: because of the "to determine at runtime" of your parent comment, I thought there could be some runtime checks (extra code) for handling enums. Yeah, I figured that's what happened and I should be more careful with my wording.
- Twisol 5y agoFrom my perspective, these really are more like enums than unions. Rust enums cover C-style enums as a limiting case: enum Result { Ok, Err, } If this was all you got, you'd need to return something like `(status, value, err)` to model fallible functions. This is not unlike Go's `value, err` convention, except that `status` is additionally inlined into `value` as a sentinel `nil`. This does nothing to prevent you from using the `value` if an error has occurred, or vice versa. Instead, we like to carry data _alongside_ the enum tags: // using named fields instead of tuple structs, for clarity enum Result<T, E> { Ok {value: T}, Err {err: E}, } Now we can return a single `Result,` and you can only use `value` if the function succeeded, and you can only use `err` if the function failed.
- yoneda 5y agoLogically speaking, Rust's "enums" are neither enums (in the traditional sense) nor unions. They are tagged unions / disjoint unions / variants / coproducts / algebraic data types. We many names for this concept, but "union" is not one of them, because a union allows for a non-empty intersection. Under the hood, of course, they are implemented with C-style unions with fields sharing memory. But to conflate Rust's enums with how they are implemented is to disregard the extra safety that they provide.
- munificent 5y agoIt has a fixed enumerated list of cases (here `Ok` and `Err`). Some of those cases may have additional payloads, but I don't see how that makes them any less like an enumerated type.