IIRC a secret 'other' field (or '__non_exhaustive' or something) is actually how we did thing before non_exhaustive was introduced.
> The word “other” means “not mentioned elsewhere”, so the presence of an Other logically implies that the enumeration is exhaustive.
In Rust, because all enums are exhaustive by default and exhaustive matching is enforced by the compiler, there is no risk of this sort of confusion. And then the fact that his proposed solution is:
> Just document that the enumeration is open-ended
The non_exhaustive attribute is effectively compiler-enforced documentation; users now cannot forget to treat the enum as open-ended.
Of course, adding non_exhaustive to Rust was not without its own detractors; it usage for any given enum fundamentally means shifting power away from library consumers (who lose the ability to guarantee exhaustive matching) and towards library authors (who gain the ability to evolve their API without causing guaranteed compilation errors in all of their users (which some users desire!)). As such, the guidance is that it should be used sparingly, mostly for things like error types. But that's an argument against open-ended enums in general, not against the mechanisms we use to achieve those (which, as you say, was already possible in Rust via hacks).
It's just that with #[non_exhaustive], you must specify a default branch (`_ => { .. }`), even if you've already explicitly matched on all the values. The idea being that you've written code which matches on all the values which exist right now, but the library author is free to add new variants without breaking your code - since it's now your responsibility as a user of the library to handle the default case.
https://doc.rust-lang.org/rustc/lints/listing/allowed-by-def...
Let's say I am using a crate called zoo-bar. Let's say this crate is not using non-exhaustive.
In my code where I use this crate I do:
let my_workplace = zoo_bar::ZooBar::new();
let mut animal_pens_iter = my_workplace.hungry_animals.iter();
while let Some(ap) = animal_pens_iter.next() {
match ap {
zoo_bar::AnimalPen::Tigers => {
me.go_feed_tigers(&mut raw_meat_that_tigers_like_stock).await?;
}
zoo_bar::AnimalPen::Elephants => {
me.go_feed_elephants(&mut peanut_stock).await?;
}
}
}
I update or upgrade the zoo-bar dependency and there's a new enum variant of AnimalPens called Monkeys.Great! I get a compile error and I update my code to feed the monkeys.
diff --git a/src/main.rs b/src/main.rs
index 202c10c..425d649 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,5 +10,8 @@
zoo_bar::AnimalPen::Elephants => {
me.go_feed_elephants(&mut peanut_stock).await?;
}
+ zoo_bar::AnimalPen::Monkeys => {
+ me.go_feed_monkeys(&mut banana_stock).await?;
+ }
}
}
Now let's say instead that the AnimalPen enum was marked non-exhaustive.So I'm forced to have a default match arm. In this alternate universe I start off with:
let my_workplace = zoo_bar::ZooBar::new();
let mut animal_pens_iter = my_workplace.hungry_animals.iter();
while let Some(ap) = animal_pens_iter.next() {
match ap {
zoo_bar::AnimalPen::Tigers => {
me.go_feed_tigers(&mut raw_meat_that_tigers_like_stock).await?;
}
zoo_bar::AnimalPen::Elephants => {
me.go_feed_elephants(&mut peanut_stock).await?;
}
_ => {
eprintln!("Whoops! I sure hope someone notices this default match in the logs and goes and updates the code.");
}
}
}
When the monkeys are added, and I update or upgrade the dependency on zoo-bar, I don't notice the warning in the logs right away after we deploy to prod. Because the logs contain too many things no one can go and read everything.One week passes and then we have a monkey starving incident at work.
After careful review we realize that it was due to the default match arm and we forgot to update our program.
So we learn from the terrible catastrophe with the monkeys and I update my code using the attributes from your link.
diff --git a/src/main.rs b/src/main.rs
index e01fcd1..aab0112 100644
--- a/wp/src/main.rs
+++ b/wp/src/main.rs
@@ -1,3 +1,5 @@
+#![feature(non_exhaustive_omitted_patterns_lint)]
+
use std::error::Error;
#[tokio::main]
@@ -11,6 +13,7 @@ async fn main() -> anyhow::Result<()> {
let mut animal_pens_iter = my_workplace.hungry_animals.iter();
while let Some(ap) = animal_pens_iter.next() {
+ #[warn(non_exhaustive_omitted_patterns)]
match ap {
zoo_bar::AnimalPen::Tigers => {
me.go_feed_tigers(&mut raw_meat_that_tigers_like_stock).await?;
@@ -18,8 +21,12 @@ async fn main() -> anyhow::Result<()> {
zoo_bar::AnimalPen::Elephants => {
me.go_feed_elephants(&mut peanut_stock).await?;
}
+ zoo_bar::AnimalPen::Monkeys => {
+ // Our monkeys died before we started using proper attributes. If they are hungry it means they have turned into zombies :O
+ me.alert_authorities_about_potential_outbreak_of_zombie_monkeys().await?;
+ }
_ => {
- eprintln!("Whoops! I sure hope someone notices this default match in the logs and goes and updates the code.");
+ unreachable!("We have an attribute that is supposed to tell us if there were any unmatched new variants.");
}
}
}
And next time we update or upgrade the crate version to latest, another new variant exists, but thanks to your tip we get a lint warning and we happily update our code so that we won't have more starving animals. diff --git a/wp/src/main.rs b/wp/src/main.rs
index aab0112..4fc4041 100644
--- a/wp/src/main.rs
+++ b/wp/src/main.rs
@@ -25,6 +25,9 @@ async fn main() -> anyhow::Result<()> {
// Our monkeys died before we started using proper attributes. If they are hungry it means they have turned into zombies :O
me.alert_authorities_about_potential_outbreak_of_zombie_monkeys().await?;
}
+ zoo_bar::AnimalPen::Capybaras => {
+ me.go_feed_capybaras(&mut whatever_the_heck_capybaras_eat_stock).await?;
+ }
_ => {
unreachable!("We have an attribute that is supposed to tell us if there were any unmatched new variants.");
}
But what was the advantage of marking the enum as #[non_exhaustive] in the first place?Each option has its place, it depends on context. Does the creator of the type want/need strictness from all their consumers, or can this call be left up to each consumer to make? The lint puts strictness back on the table as an opt-in for individual users.
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SomeEnum {
AValue,
BValue,
}
My customers use that and all is well. But I want to add a new enum value, CValue. I can't require that all my customers update their version of my Rust client before I add it; that would be unreasonable.So I add it, and what happens? Well, now whenever my customers make that API call, instead of getting some API object back, they get a deserialization error, because that enum's Deserialize impl doesn't know how to handle "CValue". Maybe some customer wasn't even using that field in the returned API object, but now I've broken their code.
Adding #[non_exhaustive] means I at least won't break my customers' code when I add a new enum value.
This allows you to do something like:
#[derive(Clone, Copy)]
#[repr(u8)]
#[non_exhaustive]
pub enum Foo {
A = 1,
B,
C,
}
impl Foo {
pub fn from_byte(val: u8) -> Self {
unsafe { std::mem::transmute(val) }
}
pub fn from_byte_ref(val: &u8) -> &Self {
unsafe { std::mem::transmute(val) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn conversion_copy() {
let n: u8 = 1;
let y = Foo::from_byte(n);
assert!(matches!(y, Foo::A));
let n: u8 = 4;
let y = Foo::from_byte(n);
assert!(!matches!(y, Foo::A) && !matches!(y, Foo::B) && !matches!(y, Foo::C));
let n2 = y as u8;
assert_eq!(n2, 4);
}
#[test]
fn conversion_ref() {
let n: u8 = 1;
let y = Foo::from_byte_ref(&n);
assert!(matches!(*y, Foo::A));
let n: u8 = 4;
let y = Foo::from_byte_ref(&n);
assert!(!matches!(*y, Foo::A) && !matches!(*y, Foo::B) && !matches!(*y, Foo::C));
let n2 = (*y) as u8;
assert_eq!(n2, 4);
}
}
This lets you have a simple fast parsing of types without needing a bunch of logic - particularly in the ref example. Someone else sent you data over the wire and is using a vendor defined value, or a newer version of the protocol that defines Foo::D? No big deal, you can igore it or error, or whatever else is appropriate for your case.If you want to define Reserved and Vendor as enum attributes, now you have to have logic that runs all the time - and if you want to preserve the original value for error messages, logs, etc - you can't Repr(u8) and take up more memory, have to do copies, etc.
#[non_exhaustive]
pub enum Foo {
Undefined =0,
A = 1,
B,
C,
Reserved(u8),
Vendor(u8),
}
impl Foo {
pub fn from_byte(val: u8) -> Self {
match val {
0 => Foo::Undefined,
1 => Foo::A,
2 => Foo::B,
3 => Foo::C,
4..=127 => Foo::Reserved(val)
128.. => Foo::Vendor(val)
}
}
}
You also need logic to convert back to a u8 now too.It's not strictly necessary, but it certainly makes some things far more ergonomic.
Apologies for my pre-coffee brainfarts.
#[non_exhaustive] is most popular for the variants of an enumeration but is permissible for published structure types (it means we promise these published fields will exist but maybe we will add more and thus change the size of the structure overall) and for the variants of a sum type (it means the inner details of that variant may change, you can pattern match it but we might add more fields and your matches must cope)
For a network protocol or C FFI you probably want a primitive integer type not any of Rust's fancier types such as enum, because while you might believe this byte should have one of six values, 0x01 through 0x06 maybe somebody decided the top bit is a flag now, so 0x83 is "the same" as 0x03 but with a flag set.
Trying to unsafely transmute things from arbitrary blobs of data to a Rust type is likely to end in tears, this attribute does not fix that.
You can practically use it today by gating on a nightly-only cfg flag. See https://github.com/guppy-rs/guppy/blob/fa61210b67bea233de52c... and https://github.com/guppy-rs/guppy/blob/fa61210b67bea233de52c...
I think half of it is developers presuming to know users' needs and making decisions for them (users can make that decision by themselves, using the default case!) but also a logic-defying fear of build breakage, to the point that I've seen developers turn other compile errors into runtime errors in order to avoid "breaking changes".
You have to opt into it but it's nice that it's available.
If I have a parameter in my public API that has enumerated options, I should be able to add a new option without needing to bump my semver major version number since downstream existing code obviously isn't going to use it yet. If downstream was using my public api's enum for some of their own book keeping and so matched on my enum, I want to reserve the right to to say that that is non-public use of my enum, hence the idea that exhaustiveness in enums is a separate decision on to what is included in a public API or not.
On the other hand, if I introduce a new variant in a return value and existing code will get it and need to actually do something with it, then it should probably be breaking. Errors are somewhat of an exception to this since almost all error enumerations need a general, "unknown error" category anyways and introducing a new variant is generally elevating one case out of that general case. Obviously authors can make mistakes.
The alternative, when you cannot mark non_exhaustive, is to introduce stringly typed catch alls, which is much less desirable for everyone.
Swift allows a ‘default’ enum case which is similar to other but you should use it with caution.
It’s better to not use it unless you’re 110% sure that there will not be additional enums added in the future.
Otherwise, in Swift when you add an additional enum case, the code where you use the enum will not work unless you handle each enum occurrence at it’s respective call site.
Swift also has two versions of a `default` case in switch statements, like you described. It has regular `default` and it has `@unknown default`. The `@unknown default` case is specifically for use with non-frozen enums, and gives a warning if you haven't handled all known cases.
So with `@unknown default`, the compiler tells you if you haven't been exhaustive (vs. the current API), but doesn't complain that your `@unknown default` case is unreachable.
The issue with traditional “default” cases is that they shadow warnings/errors about unhandled cases, but you’d still want to have some form of default case for forward compatibility.
Separate compilation is a technical implementation detail that shouldn't have an impact on semantics. Especially since LTO (link time optimisation) is becoming more and more common; 'thin' LTO is essentially free in Rust at least in terms of extra build time. LTO blurs the lines between separate compilation units.
On the flip side, Rust can use multiple codegen units even for the same crate, thus introducing separate compilation where a naive approach, like in classic C, would only use a single one.
In other words, you want to ensure that you have the most appropriate behavior for whatever values are currently known, and a fallback behavior for the future values that by definition you can’t possibly know at the present time. Of course, this is more or less only practical in languages where the interface version you compile against is only updated deliberately, while the implementation version at runtime can be any newer compatible version.
#[non_exhaustive]
pub enum Protocol {
Tcp,
Udp,
Other(u16),
}
It allows you to still match on the unrecognized case (like `Protocol::Other(1)`, which is nice), but an additional enum variant may eliminate that case, if our enum gets extended to: #[non_exhaustive]
pub enum Protocol {
Tcp,
Udp,
Icmp,
Other(u16),
}
Even though we can add additional variants in a semver-nonbreaking way due to `#[non_exhaustive]`, other people's code may now be broken until they've changed `Protocol::Other(1)` to `Protocol::Icmp`.Having had this in the back of my head for quite some time, I think instead of an `Other` case there should be two methods, one returns an `Option<Protocol>` and the other one returns the `u16` representation. Unless there's a match on one of your expected cases your default branch would inspect the raw numeric type, which would keep working even if that case is added to the enum.
F# I believe is similar wrt discriminated unions and pattern matching
By default Rust expects you to handle every enum variant. Not doing so would be a compile error.
An example - my library exposes enum Colour with 3 variants - Red, Blue Green. In your application code you `match` on all 3. So far so good. But now if I add a 4th colour to my enum, your code will no longer compile because you are no longer handling every enum variant. This is a crappy experience for the user of the library.
Instead, the library writer can make their intent clear - with the #[non_exhaustive] attribute. On such an enum it's not enough to handle the 3 colours of the enum, you must add a wildcard matcher that matches any variants added in future. This gives the library writer flexibility to make changes, while protecting the application developer from breakage.
https://protobuf.dev/best-practices/dos-donts/#unspecified-e...
It’s more complicated:
https://protobuf.dev/programming-guides/enum/
>> What happens when a program parses binary data that contains field 1 with the value 2?
>- Open enums will parse the value 2 and store it directly in the field. Accessor will report the field as being set and will return something that represents 2.
>- Closed enums will parse the value 2 and store it in the message’s unknown field set. Accessors will report the field as being unset and will return the enum’s default value.
It used to be that we broadly had two sets of semantics (modulo additional customizations): proto2 and proto3. Proto editions was supposed to unify the two versions, but instead now we have the option to mix and match all of the quirks of each of the versions.
And, to make matters worse, you also have language-dependent implementations that don't conform to the spec (in fact, very few implementations are conformant). C++ and Java treat everything imported by a proto2 file as closed; C#, Golang, and JS treat everything as open.
I don't see a path forward for removing these custom deprecated field features, or else we'd have already begun that effort during the initial adoption of editions.
I've taken to coding my C enums with the first value being "Invalid", indicating it is never intended to be created. If one is encountered, it's a bug.
This doesn’t happen when you make the first value in the enum unknown/unspecified
So any future new flavor will be read back as ‘0’ in older versions.
I’ve seen engineers bring those unknowns or unspecified through to the business logic and that always made my face flush red with anger.
If you are consuming data from some other system you have no power over what to require from users. You will have data points with unknown properties.
Say you are tracking sign ups in some other system, and they collect the users’ browser in the process, and you want to see conversion rate per browser. If the browser could not be identified, you prefer it to say ”other” instead of ”unknown”?
I think I prefer the protobuf best practices way: you have a 0 ”unknown”/”unset” value, and you enumerate the rest with a unique name (and number). The enum can be expanded in the future so your code must be prepared for unknown enumerated values tagged with the new (future for your code) number. They are all unique, you just don’t yet know the name of some of the enum values.
You can choose to not consume them until your code is updated with a more recent schema. Or you can reconcile later, annotating with the name of you need it.
Now personally, I would not pick an enum for any set och things that is not closed when you are designing. But I’m starting to think that such sets hardly exist in the real world. Humans redefine everything over time.
Possibly just showing my lack of knowledge here but are open-ended enumerations a common thing? I always thought the whole point of an enum is that it is closed-ended?
For instance, we had an enum that represented a sport that we supported. Initially we supported some sports (say FOOTBALL and ICE_HOCKEY), and over time we added support for other sports, so the enum had to be expanded.
Unfortunately this always required the entire estate to be redeployed. Thankfully this didn’t happen often.
At great expense, we eventually converted this and other enums to “open-ended” enums (essentially Strings with a bit more structure around them, so that you could operate on them as if they were “real” enums). This made upgrades significantly easier.
Now, whether those things should have been enums in the first place is open for debate. But that decision had been made long before I joined the team.
Another example is gender. Initially an enum might represent MALE, FEMALE, UNKNOWN. But over time you might decide you have need for other values: PREFER_NOT_TO_SAY, OTHER, etc.
I prefer to interpret those as an optional/nullable _closed_ enum (or, situationally, a parse error) if I have to switch on them and let ordinary language conventions guide my code rather than having to understand some sort of pseudo-null without language support.
In something like A/B tests it's not uncommon to have something that's effectively runtime reflection on enum fields too. Your code has one or more enums of experiments you support. The UI for scaling up and down is aware of all of those. Those two executables have to be kept in sync somehow. A common solution is for the UI to treat everything as strings with weights attached and for the parsers/serializers in your application code to handle that via some scheme or another (usually handling it poorly when people scale up experiments that no longer exist in your code). The UI though is definitely open-ended as it interprets that enum data, and the only question is how it's represented internally.
It is Data Model design, of which API design a subset.
You can only ever avoid having an other if 1) your schema is fixed and 2) if it is total over the universe of values.
enum WidgetFlavor
{
Vanilla,
Chocolate,
Strawberry,
NumWidgetFlavors
};
And then wherever I have switch(widgetFlavor), include static_assert(NumWidgetFlavors==4). A bit jealous of rust's exhaustive enums/matches.As far as programming languages go, all enums are explicitly open-ended in C, C++, and C#, at least, because casting an integer (of the underlying type) to enum is a valid operation.
enum WidgetFlavor
{
Vanilla,
Chocolate,
Strawberry,
Other=10000,
};
Now users can add their own (and are also responsible for making sure it works in all APIs): enum CustomWidgetFlavor
{
RockyRoad=Other,
GroovyGrape,
Cola,
};
And now you can amend the enum without breaking the client: enum WidgetFlavor
{
Vanilla,
Chocolate,
Strawberry,
Mint,
Other=10000,
};
My personal opinion would be to make the enum nullable and not add a fake value.
This is not a common thing to do, of course, but when your customers are clamoring for what would actually be a useful feature for your product, even this somewhat ugly hack is a lot better than saying "no".
So, let's say there's a function createFruit(fruitType: FruitTypeEnum);
If it's null, it seems wrong since it seems to mean that you have a fruit without type.
If it's unknown, then it might also be incorrect, since you very well know the type, it just isn't handled there.
So I'm wondering if best might be something like Unhandled, Unspecified or Unlisted.
On the other hand, if the above principle is adhered to as it should, then there is also little benefit in having an Other value. One minor conceivable benefit is that intermediate code can map unsupported values to Other in order to simplify logic in lower-level code. But I agree that it’s usually better to not have it.
A somewhat related topic that comes to mind is error codes. There is a common pattern, used for example by the HTTP status codes, where error codes are organized into categories by using different prefixes. For example in a five-digit error code scheme, the first three digits might indicate the category (e.g. 123 for “authentication errors”), and the remaining two digits represent a more specific error condition in that category. In that setup, the all-zeros code in each category represents a generic error for that category (i.e. 12300 would be “generic authentication error”).
When implementing code that detects a new error situation not covered by the existing specific error codes, the implementer has now the choice of either introducing a new error code (e.g. 12366 — this is analogous to adding a new enum value), which has to be documented and maybe its message text be localized, or else using the generic error code of the appropriate category.
In any case, when error-processing code receives an unknown — maybe newly assigned — error code, they can still map it according to the category. For example, if the above 12366 is unknown, it can be handled like 12300 (e.g. for the purpose of mapping it to a corresponding error message). This is quite similar to the case of having an Other enum value, but with a better justification.
(note that Go doesn't have enums as a language feature, but you can use its const declaration to create enum-like constants)
- Naming: "Other" should probably be called "Unrecognized" in these situations. Then users understand that members may not be mutually exclusive.
- ABI: If you need ABI compatibility, the constraint you have is "don't change the meanings of values or members", which is somewhat stronger. The practical implication is that if you do need to have an Other value, its value should be something out of range of possible future values.
- Protocol updates: If you can atomically update all the places where the enum is used, then there's no inherent need to avoid Other values. Instead, you can use compile-time techniques (exhaustive switch statements, compiler warnings, temporarily removing the Other member, grep, clang-query, etc.) to find and update the usage sites at compile time. This requires being a little disciplined in how you use the enum during development, but it's doable.
- Distributed code: If you don't have control over all the code using your enum might, then you must avoid an Other value, unless you can somehow ensure out-of-band that users have updated their code.
If, like us, you were passing the object between two applications, the owning API would serialize the enum value as a String value, then we had a client helper method that would parse the string value into an Optional enum value.
If the original service started transferring a new String object between services, it wouldn't break any downstream clients, because the clients would just end up with Optional empty
Is there a reason, aside from documentation, that this is ever desirable? I rarely program in Rust, but why would this ever be useful in practice, outside of documentation? (Seems like code-as-documentation gone awry when your code is doing nothing but making a statement about future code possibilities)
When you add #[non_exhaustive] to an enum, the compiler says to external users, "You're no longer allowed to just match every existing variant. You must always have a default 'none of the above' case when you're matching on this enum."
This lets you add more variants in the future without breaking the API for existing users, since they all have a 'none of the above' case for the new variants to fall into.
[0] https://doc.rust-lang.org/book/ch06-02-match.html#matches-ar...
I believe I've also seen this declaration for generated bindings for a JSON API that promises backwards compatibility for calls and basic functionality at least. Future versions may include more options, but the code will still compile fine against the older API.
I don't think it's a great tool to use everywhere, but there are edge cases where Rust's demand for exhaustive matches conflicts with the non-Rust world, and that's where stuff like this becomes hard to avoid.
Having such an "Other" value does not prevent from considering that the enum is open-ended, and it simplifies a lot all the code that has to deal with potentially invalid or unknown values (no need for a validity flag or null).
That's probably why in DIS (Distributed Interactive Simulation) standard, which defines many enums, all start with OTHER, which has the value zero.
In STANAGs (NATO standards), the value zero is used for NO_STATEMENT, which can also be used when the actual value is in the enum but you can't or don't need to indicate it.
I remember an "architecture astronaut" who claimed that NO_STATEMENT was not a domain value, and removed it from all the enums in its application. That did not last long.
That also reminds me of Philippe Khan (Bordland) having in some presentation the ellipse extend the circle, to add a radius. A scientist said he would do the other way around, and Khan replied: "This is exactly the difference between research and industry".
My favorite question on interviews on the OOP topic. It can be correct either way or both can be wrong, so the good answer would be "It depends". When developers rush to give a specific answer, they do not demonstrate due attention to the domain and it may mean that they will assume thousand other falsehoods from those articles on Github.
Now, they must be sure it is a signed 32bits on 32 or 64 bits systems, namely check the compiler behavior. You can check the code, they always add a 0x7fffffff as the last enum value to "force" the compiler and tell developers (which have enough experience) "hey, this is a signed 32bits"... whoopsie!
We should eat the bullet: remove the enum in vulkan3D, and use the appropriate primitive type for each platform ABI (not API...), so the "fix" should be transparent as it would no break the ABI. But all the "code generators" using khronos xml specifications and static source code are to be modified in one shot to stay consistent. This ain't small feat.
[NOTE: enum is one of those things which should be removed from the "legacy profile" of C (like tons of keywords, integer promotion, implicit cast, etc).]
const KnownFlavors {
Vanilla: "Vanilla",
Chocolate: "Chocolate",
Strawberry: "Strawberry"
}
Then, use a string to hold the actual value. doug.favoriteFlavor = KnownFlavors.Chocolate;
cindy.favoriteFlavor = "Mint"
case: KnownFlavors.Chocolate:
Expand your list of known flavors whenever you like, your system will still always hold valid data. You get all the benefits of typo-proofing your code, switching on an enum, etc., without having to pile on any wackiness to fool your compiler or keep the data normalized.It acknowledges the reality that a non-exhaustive enum isn’t really an enum. It’s just a list of things that people might type into that field.
> It acknowledges the reality that a non-exhaustive enum isn’t really an enum. It’s just a list of things that people might type into that field.
I would say the opposite, the kinds of enums that map a case to a few hardcoded branches (SUCCESS, NETWORK_ERROR, API_ERROR) are often an approximation of algebraic data types which Rust implements as enums [0] but not most languages or data formats. Since often using those will require something like a `nullthrows($response->getNetworkError())` once you've matched the enum case.
The kind of enum that's just a string whitelist, like flavors or colors, which you can freely pass around and store, likely converting it into a human-readable string or RGB values in one or two utils, is the classic kind of enum to me.
> Free-form text firle to hold the other value
> revise your enum as necessary, while migrating the dat
Both instances would defeat the purpose of an enum..
As for the original post, my 2cents are valued enums.
{
Other = 0, Vanilla = 1, Chocolate = 2, Strawberry = 3 }
In this case it allows some flexibility to add later while still being able to make use of 0 (Other). Or maybe I missed the OP's point ?
You are underestimating users' ability to ignore non strict constraints. Also when I can, why would I not increase my compile time checking ? There are very good reasons to use Enums, you seem to just want to ignore those reasons.
I acknowledge your point about the user ignoring constraints.
'0 stays as "null"-like (e.g INVALID), and '1 (which would be 0xFF in an 8 bit byte for instance) becomes "something, but I'm not sure what" (e.g. UNKNOWN).
Definitely has the same issues as referenced when needing to grow the variable, and the times where it's useful aren't super common, but I do feel like the general concept of an unknown-but-not-invalid value can help with tracking down errors in processing chains Definitely do run into the need to "beware" though with enums for sure.
struct Header
{
char waterMark[3];
uint16_t width;
uint16_t height;
uint8_t reserved[16];
}
So that you can future proof v1 binaries to still be compatible with v2 by adding empty padding on "reserved" which lets you add fields in the future. I do this sometimes and always wonder if there are other philosophies on itI wonder when we are going to re-discover OOP style dynamic dispatch (or even better: multiple dispatch) to deal with software evolution.
Initially, we didn’t include an "Other" category - which led the LLM to force-fit documents into existing types even when they didn’t belong. Obv this wasn't LLM's fault.
We realized the mistake and added "Other". This significantly improved output accuracy!
class Widget
{
WidgetFlavor Flavor; //Undefined, Vanilla, Chocolate, Strawberry
string? OtherFlavor;
}
This is easy to work from a consumer standpoint because if you have a deviant flavor to specify, you don't bother setting the Flavor member to anything at all. You just set OtherFlavor. Fewer moving pieces == less chance for bad times.The first (default) member in an enum should generally be something approximating "Undefined". This also makes working with serializers and databases easier.
At the least you want this...
enum Flavor {
Chocolate,
Banana,
Strawberry,
Other(String),
}
But that's not right either. What you really want is #[non_exhaustive]
enum Flavor {
Chocolate,
Banana,
Strawberry,
}
impl ToString for Flavor ...
Is this not a case for explicitly specifying all flavors? Other flavor has essentially introduced infinite moving pieces.
</shutting_the_fuck_up_my_wetware_machine_whispering_kek>
sr: omit other option
illuminated: add other option in front end only and alert when the backend crashes.
>Speaking at a software conference in 2009, Tony Hoare apologized for inventing the null reference, his "Billion Dollar Mistake":
>"I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." -Tony Hoare
Anders Hejlsberg brilliantly points out how JavaScript doubled the cost of that mistake:
>"My favorite is always the Billion-Dollar Mistake of having null in the language. And since JavaScript has both null and undefined, it's the Two-Billion-Dollar Mistake." -Anders Hejlsberg
>"It is by far the most problematic part of language design. And it's a single value that -- ha ha ha ha -- that if only that wasn't there, imagine all the problems we wouldn't have, right? If type systems were designed that way. And some type systems are, and some type systems are getting there, but boy, trying to retrofit that on top of a type system that has null in the first place is quite an undertaking." -Anders Hejlsberg
The JavaScript Equality Table shows how Brendan Eich simply doesn't understand equality for either data types or human beings and their right to freely choose who they love and marry:
https://dorey.github.io/JavaScript-Equality-Table/
Do any languages implement the full Rumsfeld Awareness–Understanding Matrix Agnoiology, quadrupling the cost?
Why stop at null, when you can have both null and undefined? Throw in unknown, and you've got a hat trick, a holy trinity of nihilistic ignorance, nothingness, and void! The Rumsfeld Awareness–Understanding Matrix Agnoiology breaks knowledge down into known knows, plus the three different types of unknowns:
https://en.wikipedia.org/wiki/There_are_unknown_unknowns
>"Reports that say that something hasn't happened are always interesting to me, because as we know, there are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns—the ones we don't know we don't know. And if one looks throughout the history of our country and other free countries, it is the latter category that tends to be the difficult ones." -Donald Rumsfeld
1) Known knowns: These are the things we know that we know. They represent the clear, confirmed knowledge that can be easily communicated and utilized in decision-making.
2) Known unknowns: These are the things we know we do not know. This category acknowledges the presence of uncertainties or gaps in our knowledge that are recognized and can be specifically identified.
3) Unknown knowns: Things we are not aware of but do understand or know implicitly
4) Unknown unknowns: These are the things we do not know we do not know. This category represents unforeseen challenges and surprises, indicating a deeper level of ignorance where we are unaware of our lack of knowledge.
https://en.wikipedia.org/wiki/Agnoiology
>Agnoiology (from the Greek ἀγνοέω, meaning ignorance) is the theoretical study of the quality and conditions of ignorance, and in particular of what can truly be considered "unknowable" (as distinct from "unknown"). The term was coined by James Frederick Ferrier, in his Institutes of Metaphysic (1854), as a foil to the theory of knowledge, or epistemology.
I don't know if you know, but Microsoft COM hinges on the IUnknown interface. Microsoft COM's IUnknown interface takes the Rumsfeldian principle to heart: it doesn't assume what an object is but provides a structured way to query for knowledge (or interfaces). In a way, it models known unknowns, since a caller knows that an interface might exist but must explicitly ask if it does.
Then there's Schulz's Known Nothing Nesiology, representing the existential conclusion of all this: when knowledge itself is questioned, where does that leave us? Right back at JavaScript's Equality Table, which remains an unfathomable unknown unknown to Brendan Eich and his well known but knowingly ignorant War on Equality.
https://www.youtube.com/watch?v=HblPucwN-m0
Nescience vs. Ignorance (on semantics and moral accountability):
https://cognitive-liberty.online/nescience-vs-ignorance/
>From a psycholinguistic vantage point, the term “ignorance” and the term “nescience” have very different semantic connotations. The term ignorance is more generally more widely colloquially utilized than the term nescience and it is often wrongly used in contexts where the word nescience would be appropriate. “Ignorance” is associated with “the act of ignoring”. Per contrast, “nescience” means “to not know” (viz., Latin prefix ne = not, and the verb scire = “to know”; cf. the etymology of the word “science”/prescience).
>As Mark Passio points out, the important underlying question which can be derived from this semantic distinction pertains to whether our individual and global problems are caused by “ignorance” or “nescience”? That is, “ignoring” or “not knowing”? It seems clear that it is the later. We know about the truth but we actively ignore it for the most part. Currently people have all the necessary information available (literally at their fingertips). Ignoring the facts is a decision, an irrational decision, and people can be held accountable for this decision. Nescience, on the other hand, acquits from accountability (i.e., someone cannot be held accountable when he/she for not knowing something but for ignoring something). Quasi-Freudian suppression plays a pivotal role in this scenario. Suppression is very costly in energetic terms. The energy and effort which is used for suppression lacks elsewhere (cf. prefrontal executive control is based on limited cognitive resources). The suppression of truth through the act of active ignoring thus has negative implications on multiple levels – on the individual and the societal level, the cognitive and the political, the psychological and the physiological.
Brendan: While we can measure the economic consequences of your culpably ignorant mistakes of both bad programming language design and marriage inequality in billions of dollars, the emotional, social, and moral costs of the latter -- like diminished human dignity and the perpetuation of discrimination -- are, by their very nature, priceless.
Ultimately, these deeper impacts underscore that the fight for marriage equality, defending against the offensive uninvited invasion of your War on Equality into other people's marriages, is about much more than economics; it’s about ensuring fairness, respect, and equality for all members of society.