dart-lang / language

Design of the Dart language
Other
2.61k stars 200 forks source link

Expose interfaces for enums and enum items for generic programming #1007

Open raveesh-me opened 4 years ago

raveesh-me commented 4 years ago

This is the test case I want to satisfy:

enum TestEnum { anda, murgi }
void main() {
  group("EnumConverter should convert to and back between enum and string", () {
    // setup
    EnumConverter<TestEnum> testEnumConverter = EnumConverter<TestEnum>();
    test("toJson(TestEnum.anda) should return 'anda'", () {
      expect(testEnumConverter.toJson(TestEnum.anda), equals("anda"));
    });
  });
}

highlight:

      expect(testEnumConverter.toJson(TestEnum.anda), equals("anda"));

I want to write a converter that can take the type of enum and it is impossible to get the enum interface in the type.

In my ideal case scenario, a solution like this should have worked:

abstract class EnumInterface<T> {
  List<EnumItemInterface> get values;
}

abstract class EnumItemInterface {
  int get index;
  String toString();
}

class EnumConverter<T> implements JsonConverter<T, String> {
  @override
  T fromJson(String json) {
    return (T as EnumInterface<T>)
        .values
        .firstWhere((element) => element.toString().endsWith(json)) as T;
  }

  @override
  String toJson(T json) {
    // just make sure there are no '.' s in the enum class name or this will fail.
    return (json as EnumItemInterface).toString().split('.')[1];
  }
}

Unfortunately, casts like JSON as EnumItemInterface are not as mindless in dart and I am met with an error:

type 'TestEnum' is not a subtype of type 'EnumItemInterface' in typecast

Enums and items in the enums already implement a fixed interface. Just exposing those interfaces in the SDK will give me enough to work with, enabling generic programming with enum classes.

A way to impose interfaces on top of classes that we know have that interface but are cast as not having it would also be cool.

kevmoo commented 2 years ago

We got closer in 2.14 with this. There is an Enum base class that exposes an index property.

CC @lrhn if there is anything left here of if we could/should close

lrhn commented 2 years ago

With the Enum superclass we can do the toJson method. With a hopefully up-coming name getter on Enum, we can even do it efficiently (without using split or even any new allocation).

We still can't access the values of an enum type given a type variable, which is needed for fromJson. It's a feature we can consider, say as a static List<T> valuesOf<T extends Enum>() on Enum. The risk is that it might make tree-shaking the values list of enum classes less efficient. The benefit is that it allows methods like fromJson or a general efficient class EnumSet<T extends Enum> It's not something we're working actively on, and it does require compiler/back-end support.

xuanswe commented 2 years ago

I can use enum generic for simple case:

abstract class Layout<B extends Enum> {
  abstract final Map<B, double> breakpoints;
}

enum MaterialBreakpoint { sm, lg }
class MaterialLayout extends Layout<MaterialBreakpoint> {}

enum BootstrapBreakpoint { sm, lg, xl }
class BootstrapLayout extends Layout<BootstrapBreakpoint> {}

Now, I want to have common methods on these enums, I must do it like this:

extension MaterialBreakpointExt on MaterialBreakpoint {
  bool operator <(MaterialBreakpoint breakpoint) => index < breakpoint.index;
  bool operator >(MaterialBreakpoint breakpoint) => index > breakpoint.index;
}

extension BootstrapBreakpointExt on BootstrapBreakpoint {
  bool operator <(BootstrapBreakpoint breakpoint) => index < breakpoint.index;
  bool operator >(BootstrapBreakpoint breakpoint) => index > breakpoint.index;
}

How could I allow inheritance for enums in the example above? The code snippet below explains what I mean. In the snippet, I use comparison as an example, but I mean arbitrary methods in general.

abstract enum Breakpoint {}

extension BreakpointExt<T extends Breakpoint> on T {
  bool operator <(T breakpoint) => index < breakpoint.index;
  bool operator >(T breakpoint) => index > breakpoint.index;
}

enum MaterialBreakpoint extends Breakpoint { sm, lg }
bool compareBreakpoints() {
  return MaterialBreakpoint.sm > MaterialBreakpoint.lg;
}
Levi-Lesches commented 2 years ago

@nguyenxndaidev, if you're problem is that you don't want to write another extension for each enum, can you use on Enum?

extension Comparison<T extends Enum> on T {
  bool operator <(T other) => index < other.index;
  bool operator >(T other) => index > other.index;
}

enum Letters {a, b, c}
enum Colors {red, green, blue}

void main() {
  print(Letters.a > Letters.b);  // false
  print(Letters.a < Letters.b);  // true
  print(Letters.a > Colors.red); // error, type mismatch
}
xuanswe commented 2 years ago

@Levi-Lesches In the snippet, I use comparison as an example, but I mean arbitrary methods in general.

I don't want to make these extension methods for all enums, only on specific enums. These enums are defined by users, not me (owner of the library). That's why I don't want to use extends Enum and on Enum.

Levi-Lesches commented 2 years ago
  1. Enums are just collections of constants. If you want them to mean something more than that (ie, have arbitrary methods available for some types), just make them actual classes with static constants (an "enum-like class")
  2. You can define functions and allow the user to apply them on extensions themselves:
    
    // in your library
    bool lt<T extends Enum>(T a, T b) => a.index < b.index;
    bool gt<T extends Enum>(T a, T b) => a.index > b.index;

// in the user's code extension on Letters { bool operator <(Letters other) => lt(this, other); bool operator >(Letters other) => gt(this, other); }

enum Letters {a, b, c} enum Colors {red, green, blue}

void main() { print(Letters.a > Letters.b); // false print(Letters.a < Letters.b); // true print(Letters.a > Colors.red); // error, type mismatch print(Colors.red > Colors.green); // error, Colors doesn't have > }

xuanswe commented 2 years ago
  1. Enums are just collections of constants. If you want them to mean something more than that (ie, have arbitrary methods available for some types), just make them actual classes with static constants (an "enum-like class")

Could you give me a code snippet? Note that, the list of values in user defined enum-like class in are different.

  1. You can define functions and allow the user to apply them on extensions themselves:

This option doesn't make sense to me.

Levi-Lesches commented 2 years ago

Could you give me a code snippet?

What I'm trying to say is, without context, it sounds like you're trying to do too much with enums (you've mentioned "inheritance", "arbitrary methods", "defined by the user", and "generics"). After all, concepts like inheritance and subtypes don't really apply to enums.

Note that, the list of values in user defined enum-like class in are different.

Not sure what you mean by this -- static const values are identical to enums for practical purposes. Perhaps something like this for your example:

class MaterialBreakpoint {
  final int index;
  final double size;
  const MaterialBreakpoint(this.index, this.size);

  bool operator <(MaterialBreakpoint other) => index < other.index;
  bool operator >(MaterialBreakpoint other) => index > other.index;

  static const sm = MaterialBreakpoint(0, 10);
  static const lg = MaterialBreakpoint(1, 20);
}

class BootstrapBreakpoint extends MaterialBreakpoint {
  final String device;
  const BootstrapBreakpoint(int index, double size, this.device) : super(index, size);

  static const xl = BootstrapBreakpoint(2, 30, "Web on Desktop");
}

void main() {
  print(MaterialBreakpoint.sm < MaterialBreakpoint.lg);
  print(BootstrapBreakpoint.lg > MaterialBreakpoint.sm);
}
lrhn commented 2 years ago

If we add something like #158, we may also allow enum declarations to implement interfaces. Then you can do:

enum MaterialBreakpoint implements OrderedEnum {
  lg, sm;
}
abstract class OrderedEnum {
  int get index;
}
extension OrderedEnumOrder<T extends OrderedEnum> on T {
  bool operator <(T other) => index < other.index;
  bool operator >(T other) => index > other.index;
  bool operator <=(T other) => index <= other.index;
  bool operator >=(T other) => index >= other.index;
}
xuanswe commented 2 years ago

@Levi-Lesches Thanks for your suggestions! I will see if I can wrap enum inside an abstract class before enum in dart becomes more powerful.

@lrhn your solution is what I need. The key is to allow enum to implement an interface. I hope that it will be added to Dart soon.