language icon indicating copy to clipboard operation
language copied to clipboard

Allow accessing `Enum.values` inside extensions and mixins

Open FMorschel opened this issue 1 year ago • 8 comments

Today, doing this is not allowed. We get: An instance member named 'values' can't be declared in a class that implements 'Enum'. Try using a different name. (illegal_enum_values)

mixin M1 on Enum {
  List<Enum> get values;
}

But when we try to do either of the following we get: Undefined name 'values'. Try correcting the name to one that is defined, or defining the name. (undefined_identifier)

mixin M2 on Enum {
  void foo() {
    print(values);
  }
}

extension EE1<T extends Enum> on T {
  void bar() {
    print(values);
  }
}

Although, the following is valid and I believe this is a bug but someone can correct me if I'm wrong or confirm so I can create a new issue:

extension EE2<T extends Enum> on T {
  List<T> get values => [];
}

With the code above, both M2 and EE1 stop warning for undefined_identifier.

So what I'm asking here is that we give out the same error for EE2 and M1 but allow the user to access values. This would help for example if someone wants to do an extension like:

extension ForwardBackwardExt<T extends Enum> on T {
  T? get next {
    final nextIndex = index + 1;
    if (nextIndex > (values.length - 1)) return null;
    return values[nextIndex];
  }

  T? get previous {
    final previousIndex = index - 1;
    if (previousIndex < 0) return null;
    return values[previousIndex];
  }
}

For me, what I did in this case, was the following workaround, but it needs me to implement ForwardBackwardMixin in every enum I want this and is not ideal if I'd like this to work on anything:

mixin ForwardBackwardMixin<T extends Enum> on Enum {
  @protected
  List<T> get staticValues;
}

extension ForwardBackwardExt<T extends ForwardBackwardMixin<T>> on T {
  T? get next {
    final nextIndex = index + 1;
    if (nextIndex > (staticValues.length - 1)) return null;
    return staticValues[nextIndex];
  }

  T? get previous {
    final previousIndex = index - 1;
    if (previousIndex < 0) return null;
    return staticValues[previousIndex];
  }
}

FMorschel avatar Aug 15 '24 11:08 FMorschel

As far as I understand, values is not part of the Enum interface. This is the reason why you can't use it in an extension, mixin, or anything that receives an Enum.

What does happen is that, every time we declare an enum Foo, an abstract final class is internally created that contains the static getter values.

To make what you want, Enum would have to change its definition to Enum<T extends Enum>, and provide a values getter of type List<T>, so that the enum Foo extends Enum<Foo> and the values field is available in the instances.

I am not sure why mixins and extensions disallow declaring fields with type values, tho, considering that the values getter are defined in the "namespace", not in the instance.

mateusfccp avatar Aug 15 '24 13:08 mateusfccp

I am not sure why mixins and extensions disallow declaring fields with type values, tho, considering that the values getter are defined in the "namespace", not in the instance.

Sorry if I have not made myself clear. Today only Mixins disallow that. That was something I intended to review to be more consistent between both Mixins and extensions. See:

Although, the following is valid and I believe this is a bug but someone can correct me if I'm wrong or confirm so I can create a new issue:

extension EE2<T extends Enum> on T {
  List<T> get values => [];
}

With the code above, both M2 and EE1 stop warning for undefined_identifier.

So what I'm asking here is that we give out the same error for EE2 and M1 [...]

FMorschel avatar Aug 15 '24 13:08 FMorschel

@mateusfccp is essentially correct, values is a static member and thus can only be referenced through the explicit type. You can use the bare values identifier within the enum itself only because static members are in the lexical scope in that case.

In other words, it is as if you have a class like this:

class A {
  static List<String> get values => ['a', 'b', 'c'];
}

extension A2<T extends A> on T {
  List<String> get x => values; // Error, `values` must be qualified with an explicit type
}

You could create a separate interface or something, which requires an instance member on any enum provided to it, which will return the values, if you really need this:

abstract interface class EnumWithEntries<T extends Enum> implements Enum {
  List<T> get entries;
}

enum A implements EnumWithEntries<A> {
  a, b;
  
  List<A> get entries => A.values;
}

extension E2<T extends EnumWithEntries<T>> on T {
  List<T> get x => entries; 
}

jakemac53 avatar Aug 15 '24 16:08 jakemac53

Yes, that's what I do in my workaround at the end of the OP.

But to my point, somehow mixins know that they must not create a values member.

Extensions should receive the same warning as the mixins when creating a member values.

Today, as I stated in the OP, I can create an extension with a values member and that would make any other extensions or mixins that have it in scope behave somewhat unexpectedly.

[...] both M2 and EE1 stop warning for undefined_identifier.

But then, if I know that I must not do that inside enum mixins, why can't that value be provided in that context?

FMorschel avatar Aug 15 '24 16:08 FMorschel

Yes, that's what I do in my workaround at the end of the OP.

Sorry I missed that.

But to my point, somehow mixins know that they must not create a values member.

Enums are not allowed to ever have a values instance member, so we know that a mixin on Enum cannot have one, it can never be successfully applied if it has a values member, so it is an unusable mixin.

Extensions are different, those are not instance members. They are just syntax sugar for static functions. I see what you are saying that it can create potentially confusing code. I am not sure we could reasonably fix this, because you could always make an extension on Object or something else, which would have the same issue (since it would also apply to enums).

But then, if I know that I must not do that inside enum mixins, why can't that value be provided in that context?

Because it is a static function, it can't be looked up without the explicit type. You cannot invoke static functions on a type from an instance of that type.

If we had "static interfaces" we could possibly make it work, and there are other reasons to want it to be sure. And then you would do something like T.values.

jakemac53 avatar Aug 15 '24 17:08 jakemac53

I see. Thanks for the clarification. My next question is "where"/"how" Enum.values is defined.

My question would be more like "Could we add that same definition/creation as a static value for enum mixin/extension"? As a "syntax sugar" for the current mixin/extension name.

enum E with M {
  //...
}

mixin M on Enum {
  // Defining a `M.values` that returns `List<M>` which we know would be overridden for every `with`
}

extension EE on E {
  // Defining a `EE.values` that returns `List<EE>` which we know would be overridden for every `with`
}

Something like this.

FMorschel avatar Aug 15 '24 17:08 FMorschel

I see. Thanks for the clarification. My next question is "where"/"how" Enum.values is defined.

https://spec.dart.dev/DartLangSpecDraft.pdf page 81 shows the class equivalent that an Enum desugars to. It is just compiler magic. This isn't updated yet for enhanced enums, but the feature spec has references to some of the specific errors around values members. It also more explicitly describes the static constant values variable and general desugaring better than the last published spec.

My question would be more like "Could we add that same definition/creation as a static value for enum mixin/extension"? As a "syntax sugar" for the current mixin/extension name.

This would have to be specialized for every generic instantiation that exists - or every mixin application. It can't be expressed in terms of the current language, and so it isn't just a simple desugaring like how values is created. So while anything is possible I think it would be better to add static interfaces to the language, which is a more general solution to this type of problem.

jakemac53 avatar Aug 15 '24 17:08 jakemac53

What @jakemac53 says. This is working as intended, no bugs.

You cannot declare a static member with the same name as an instance member of the same class. Since all enum classes get a static values member, they cannot have an instance member named values. And any interface or mixin which implements Enum is only usable as a supertype of an enum declaration, so they are preemptively prevented from declaring an instance values member.

Extension member declarations are not instance members. They're declared separately, and the class doesn't know that extensions are declared on it. An extension member can have the same name as a static member of the class they're on. Partly because extensions apply to all subtypes too, which may not have that static member, and because otherwise it would be a breaking change to add a static member to a class.

The restriction really is to avoid having both a static member in scope, and an instance member either declared in the same scope or assumed available through an implicit this. access. It only applies to the class where the static member is declared. For enums, the restriction on interfaces implementing Enum is to eagerly prevent enum classes from having entirely predictable errors.

lrhn avatar Aug 16 '24 08:08 lrhn