Skip to content
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

enums extending other types #3780

Open
nate-thegrate opened this issue May 4, 2024 · 5 comments
Open

enums extending other types #3780

nate-thegrate opened this issue May 4, 2024 · 5 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@nate-thegrate
Copy link

Proposal

Allow the following as valid dart code:

enum MyEnum extends Foo { ... }



My understanding is that the enum keyword desugars to something like

class MyEnum extends _Enum { ... }

when instead, it could be

@EnumMacro()
class MyEnum implements Enum { ... }



With this change, you could make enumerated collections for widgets, colors, decorations, text styles… anything with a const constructor!

- enum NavBarButton {
+ enum NavBarButton extends StatelessWidget {
  home(Icons.home),
  leaderboard(Icons.leaderboard),
  schedule(Icons.calendar_month),
  profile(Icons.account_circle);

  final IconData icon;
  const NavBarButton(this.icon);

+  @override
-  Widget get destination {
+  Widget build(BuildContext context) {
    return NavigationDestination(icon: Icon(icon), label: name);
  }
}

before

NavigationBar(destinations: [for (final button in NavBarButton.values) button.destination]);

after

NavigationBar(destinations: NavBarButton.values);

I'd love to see this implemented as part of the code generation release.

@nate-thegrate nate-thegrate added the feature Proposed language feature that solves one or more problems label May 4, 2024
@lrhn
Copy link
Member

lrhn commented May 5, 2024

It's not impossible.

We currently make enum classes extend Enum (implemented as extending _Enum which extends Enum) because it gives us a shared implementation of index and toString, and more importantly, of _name, which is used to implement toString and the extension getter name.

Then we allow mixins on top of Enum, which means we have to disallow overriding index, == and hashCode, and having a member named values that would prevent creating the static getter.

If instead we allowed extending any class, and instead introduced the Enum implementation as a further mixin, then it should be able to work the same.
What lacks for us to be able to do that, is constructors on mixins. We cannot introduce the shared index and _name fields using a mixin and initialize them in the constructor.

We could just introduce the fields in each enum class directly, but being able to share the fields is important for code size.

We could perhaps introduce a hack that allowed initializing the mixin, without adding it as a general language feature.
I'd prefer to not implement half a feature, and risk having to reimplement it anyway if we do the full feature.

So unless the priority of this request increases, I'll expect it to be blocked on adding constructors to mixins. If we have that, it becomes a much smaller feature to allow superclasses for enums.
(And allowing static and instance members with the same name would avoid some of the name clash issues.)

@nate-thegrate
Copy link
Author

@lrhn thanks very much for the reply.

Enum implementation as a further mixin

I'm wondering if it's possible to implement enums using a macro rather than a mixin.

I don't really understand how the enum keyword works under the hood, but I believe the current implementation is something like this:

abstract interface class Enum {
  int get index;
  String get _name;
}

abstract class _Enum implements Enum {
  @override
  final int index;
  @override
  final String _name;

  const _Enum(this.index, this._name);

  @override
  String toString() => '$runtimeType.$_name';
}

And when you declare an enhanced enum

enum MyEnum {
  a(true),
  b(false),
  c(null);

  const MyEnum(this.value);
  final bool? value;
}

it's interpreted as follows:

class MyEnum extends _Enum {
  static const a = MyEnum(0, 'a', true);
  static const b = MyEnum(1, 'b', false);
  static const c = MyEnum(2, 'c', null);

  static const values = [a, b, c];
  
  const MyEnum(super.index, super._name, this.value);
  final bool? value;
}



What if we got rid of _Enum and used a macro class instead?

@EnumMacro()
class MyEnum {
  static const a = MyEnum(true);
  static const b = MyEnum(false);
  static const c = MyEnum(null);

  static const values = [a, b, c];
  
  const MyEnum(this.value);
  final bool? value;
}

And the EnumMacro class would transform it into this:

augment class MyEnum implements Enum {
  static const a = MyEnum(0, 'a', true);
  static const b = MyEnum(1, 'b', false);
  static const c = MyEnum(2, 'c', null);

  static const values = [a, b, c];
  
  const MyEnum(this.index, this._name, this.value);
  final bool? value;

  @override
  final int index;
  @override
  final String _name;

  @override
  String toString() => '$runtimeType.$_name';
}

It seems to me like this could work without a need for mixin constructors, but there's a good chance that I'm misunderstanding some part of it.

@lrhn
Copy link
Member

lrhn commented May 5, 2024

Using a macro, or just generating the fields directly on the enum class has two problems:

  1. Not being able to access the private _name field.
  2. Duplicating code that could be shared.

The second point affects the code size of web compiles, probably all AoT compilers. Using the same private field for everything also keeps the code monomorphic.
I guess the compilers could do something special-cased for both, but today no special casing is needed.

@nate-thegrate
Copy link
Author

Sounds good, thanks for the info!

@FireSourcery
Copy link

FireSourcery commented May 14, 2024

Combined mixins would address this issue as well..

The current work around for effectively extending an Enum type is either, extension methods on an Enum subtype, or abstract mixin class X implements Enum. Naturally the next question is, can one be consider a best/better practice?

It can be left up to the user to essentially split implementation into 2 parts, a mixin for type constraint, and mixin[s] for methods implementations.

abstract mixin class EnumField implements Enum {}, 
abstract mixin class XFieldMethods implements EnumField {}

enum XField with XFieldMethods, EnumField, ...

layered/multiple inheritance is reflected in the with clause, although type constrained for correctness, can become cumbersome.

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

3 participants