Backward compatibility of enums

In rust, if a crate publishes an enum, then adding variant to the enum is a breaking change. Because if the dependent crate does a match against value of that enum type, and that match does not have a default (_) branch, because it covers all existing variants, then the match will break by extending the enum.

So what should a crate do to leave itself the option to extend the enum later?

The above is the only case I could think of, but there may be others. Is there perhaps some way to put a private dummy value in the enum that can't be matched explicitly, thus forcing a default branch in each match? If there isn't, should it be proposed?

If all the variants have the same parameters, a tuple(struct) with integeral tag can probably be used instead. But if the parameters are not regular…

I sometimes have the opposite problem: the presence of the _ catchall sometimes makes my code compile even if I have added a new variant to an enum. That's why I try to minimize the number of _ in my match{}.

There has been talk (maybe an RFC as well?) about the ability to tag enums as "extensible", where other crates can't match against them without a wildcard entry but I don't think it's gone anywhere. Might be a reasonable time to start pushing on it again.

I the current workaround is a "secret" variant:

pub enum Foo {
    A,
    B,
    #[doc(hidden)]
    ___ForExtensibility,
}
2 Likes

extensible enums RFC

2 Likes

Another detail, if you don't know if you want to add fields in the future, the hidden variant should have a field too (for example (), but even better Void), so that the enum is not castable to integers.

2 Likes

Isn't this actually a good thing? The compiler can tell the user that they should now stop and think how to support a new case. That's the whole purpose of exhaustiveness checking.

There's a very ugly way for the user to opt-out from the exhaustiveness checking:

enum E { A, B, C }

fn foo(e: E) -> i32 {
    match e {
        E::A => 0,
        E::B => 1,
        ref r_e if match r_e { &E::C => true, _ => false } => 2,
        _ => -1
    }
}

It can be simplified to

fn foo(e: E) -> i32 {
    match e {
        E::A => 0,
        E::B => 1,
        e if e as usize == E::C as usize => 2,
        _ => -1
    }
}

assuming E is a C-like enum and a Copy.

Not always.

If you can just bump the major version, the dependent crates will keep using the old one until they are updated and for updating them it is a good thing. But you've bumped the major version. It is not backward compatible.

However in some cases, you don't want to bump the major version. Either because it is actually a minor update that 99% of users won't really care about anyway, because you don't want users to end up with multiple versions of your crate linked in e.g. because you have static variables that should remain shared, or because of both.

2 Likes

You're talking about an enum against which the library users already wrote an exhaustive match, right? I don't quite see how 99% of them can be indifferent with the additional variants under such a situation. I'd assume users will either massively ignore many variants with if let or _, or examine every possible cases with a exhaustive match. Maybe I'm not correct here.

Well, that can happen anytime with any reasons and a library writer has no means for preventing it. Isn't it a fundamental flaw of your hypothetical crate?

I guess Ocaml community has some practical guidelines which might interest you, if it wasn't "use polymorphic variants (which Rust doesn't have.)"

This has a precedent in std::io::ErrorKind which does exactly this. Since it's libstd, it has the privilege of using #[unstable] to enforce it.

I think it's an important pattern. I've used it. The docs say the enum does not have an exhaustive definition, so that should make it 100% clear an exhaustive match is not supported and may break if you hack around it.

2 Likes

No. I am talking about an enum that does not even exist yet.

There are many formats and protocols specified with “application SHALL ignore any attributes it does not understand”. This is similar. Either because the enum represents attributes of some such format, or because there are some widely useful variants and then some additional variants for rare cases.

There is no way to totally prevent it, but at least I can eliminate most of them by keeping it at version 1.0.z and only incrementing the patchlevel version. But that means no breaking changes.

It has been postponed. There is a tracking issue for it, rfcs#943, which is open for a year with no feedback.

It's also used in regex: https://github.com/rust-lang-nursery/regex/blob/master/src/error.rs#L34