Skip to content

Make it possible to mark an enum as "extensible" #2968

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
stereotype441 opened this issue Mar 30, 2023 · 4 comments
Open

Make it possible to mark an enum as "extensible" #2968

stereotype441 opened this issue Mar 30, 2023 · 4 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@stereotype441
Copy link
Member

(Based on a discussion with the analyzer team this morning)

In Dart 3.0, with the addition of support for patterns, it has become a compile-time error if the scrutinee of a switch has an enum type, and the switch isn't exhaustive. This means that if a package publishes an enum type as part of its public API, adding a value to that enum is a breaking change.

(Previous to Dart 3.0, it was still technically breaking, however the breakage scenario was less common: a problem would only occur if the new enum value opened up a control flow path that triggered some other compile-time error. For example, adding the new enum value might have prevented a function from returning a value, or prevented a local variable from being definitely assigned.)

Some package authors want to be able to add values to existing public enums without breaking clients, for example the analyzer team would like to be able to add values to the meta package's TargetKind enum without having to do a major version bump. Currently they're considering changing the enum to a class with static constant fields, so that switches on TargetKind will no longer be required to be exhaustive (based on a suggestion from @lrhn).

It would be nice if instead there were some way to tell the compiler that a certain enum is "extensible". We might do this, for example, with a keyword, e.g.:

extensible enum Foo {
  a,
  b
}

or perhaps the ... symbol could be used to indicate that more enum values are likely to be added in the future, i.e.:

enum Foo {
  a,
  b,
  ...
}

Making an enum extensible would suppress the logic in the exhaustiveness algorithm that allows a switch to be exhaustive if it mentions all possible enum values. However, the enum would still be considered an "always exhastive" type. That means that switches would be required to include either a default case or a case that matches the enum using _. For example, this would become a compile-time error:

f(Foo foo) {
  switch (foo) {
    case Foo.a:
      print('Foo.a');
    case Foo.b:
      print('Foo.b');
  }
}

And the client would have to do something like this instead:

f(Foo foo) {
  switch (foo) {
    case Foo.a:
      print('Foo.a');
    case Foo.b
      print('Foo.b');
    default:
      print('Some other value of Foo');
  }
}

CC @dart-lang/language-team

@stereotype441 stereotype441 added the feature Proposed language feature that solves one or more problems label Mar 30, 2023
@jakemac53
Copy link
Contributor

jakemac53 commented Mar 30, 2023

This was a warning previously (or some sort of diagnostic at least), which was good because you would at least be notified when some new value was added, where the default case solution would hide the fact that you aren't covering all values.

Is there a way we can retain that behavior as well?

@lrhn
Copy link
Member

lrhn commented Mar 30, 2023

The "obvious" syntax would be sealed enum for the current behavior, and enum for the rest.

An alternative is to not use enum at all, but allow any class to declare canonical instances:

final class MyEnumLikeThing {
  v1(1), v2(2), v3(3);  // <- new!
  final int value;
  const MyEnumLikeThing(this.value);
}

The canonical values list is what makes an enum convenient for declaring a number of instances, the sealing and exhaustiveness is not syntactic. If you don't want the exhaustivness (or even the sealing, you don't have to make it final), all you need is the convenient instance creation.

It's ... not a convenient syntax, though. Probably too hard to parse without a lot of look-ahead.

So maybe allow you to write enum class, which is just a class with a canonical-elements section at the front, which creates static constant values, like in an enum declaration.
The class won't get a static values or index, won't extend Enum, and nothing prevents calling the constructor to create other instances (but you can add index and values yourself, and you can make the constructor private if you want to).
And no exhaustiveness, because nothing prevents creating more instances.

final enum class MyEnumLikeThing {
  v1._(1), 
  v2._(2), 
  v3._(3);
  final int value;
  const MyEnumLikeThing._(this.value);
}

If we think the primary/only use-case is to create non-exhaustive enum-like value sets, the analyzer could choose to give hints if a switch over the instances isn't complete. But that might just prevent other reasonable uses.

@leafpetersen
Copy link
Member

For switch expressions, a default case will have to be added no matter what. For switch statements, do you want users to have a default case (possibly that throws), or are you comfortable with them falling off the end when you add a value to the enum.

I wonder whether the nicest thing to do here might not just be:

enum Foo {
  a,
  b,
  _reservedForFutureCases
}

Which will force all switch expressions and statements to have a default case for this type.

@stereotype441
Copy link
Member Author

For switch expressions, a default case will have to be added no matter what. For switch statements, do you want users to have a default case (possibly that throws), or are you comfortable with them falling off the end when you add a value to the enum.

Personally I would prefer that they have a default case, which was why I proposed that extensible enums would still be considered "always exhaustive" types. But I don't feel too strongly about this particular detail of the proposal.

I wonder whether the nicest thing to do here might not just be:

enum Foo {
  a,
  b,
  _reservedForFutureCases
}

Which will force all switch expressions and statements to have a default case for this type.

Oh, that's clever! I will mention this to the analyzer team 😃.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

4 participants