Skip to content

Missing possibility to associate values to keys in the enum. #33698

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

Closed
dkraczkowski opened this issue Jun 29, 2018 · 37 comments
Closed

Missing possibility to associate values to keys in the enum. #33698

dkraczkowski opened this issue Jun 29, 2018 · 37 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). type-enhancement A request for a change that isn't a bug

Comments

@dkraczkowski
Copy link

dkraczkowski commented Jun 29, 2018

I couldn't find relevant issue which is strange, so if it already exists please pardon me.
Many people are using enums as bit flags (including myself) and in this case scenario current enum implementation is lacking functionality that allows you to assign relevant value to an attribute, eg:

enum Role {
  NormalUser = 1;
  Admin = 2;
  Fiance = 4;
  Bob = Fiance & Admin;
}

Langauges like c# and java do support this feature, considering fact that dart is taking the best of all modern languages I am finding the above a bit disturbing.
Any idea if this is something that dart will support in near future?

@dkraczkowski dkraczkowski changed the title Missing possibility to associate values in the enum. Missing possibility to associate values to kyes in the enum. Jun 29, 2018
@dkraczkowski dkraczkowski changed the title Missing possibility to associate values to kyes in the enum. Missing possibility to associate values to keys in the enum. Jun 29, 2018
@lrhn
Copy link
Member

lrhn commented Jun 29, 2018

Unlikely to be "near future". This has been requested for a while, at least since #21966.

What you are asking for here is really just integer constants and a type alias for them. The constants we can already do:

class Role {
   static const NormalUser = 1;
   static const Admin = 2;
   static const Fiance = 4;
   static const Bob = Fiance & Admin;
}

@lrhn lrhn added area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). type-enhancement A request for a change that isn't a bug labels Jun 29, 2018
@zoechi
Copy link
Contributor

zoechi commented Jun 29, 2018

Perhaps just adding support for switch/case for old-style enums would be the better option ;-)
Are there any other limitations (except that it's a bit more verbose but much more flexible and powerful)?

@dkraczkowski
Copy link
Author

dkraczkowski commented Jun 29, 2018

I can live without the feature- seems like some people are actually against this use-case scenario (which does not make sense to me), but if there is a roadmap with small help I think I would be able to add this feature to darts' vm.

@lrhn
Copy link
Member

lrhn commented Jul 2, 2018

I'm not opposed to having value-based enums in addition to class based enums. I'd also like to have more expressive class based enums (like Java's). I even think it's possible to have both, by making value-based enums use = to signify the value:

enum Foo { // value based, first entry needs `=` or defaults to zero.
  foo = 42,
  bar,  // defaults to 43, works for integer values only.
  baz = 37
}
enum Bar implements Comparable<Bar> {  // class based
  foo, 
  bar, 
  baz
  int compare(Bar other) => this.index - other.index;
}

All in all, I just don't think value-based enum syntax brings a lot of value compared to the existing alternative (static constants), so I don't see it as a high priority.

@izayl
Copy link

izayl commented Feb 27, 2019

Unlikely to be "near future". This has been requested for a while, at least since #21966.

What you are asking for here is really just integer constants and a type alias for them. The constants we can already do:

class Role {
   static const NormalUser = 1;
   static const Admin = 2;
   static const Fiance = 4;
   static const Bob = Fiance & Admin;
}

@lrhn this solution are not suit for type check. Think this scene.

enum Speed {
    SLOW = 0.5,
    MEDIUM = 1,
    FAST = 1.5
}

class  Vehicle {
    final Speed _speed;
    Vehicle({ this._speed = Speed.SLOW });
}

Class is not fit this type check scene.

@eernstg
Copy link
Member

eernstg commented Feb 27, 2019

@izayl wrote:

not suit for type check

If you want to use a double representation for a new type (such that the type checker can flag usages where "this kind of double" is mixed with "other kinds of double") then you're basically asking for a type branding mechanism.

Dart doesn't have that (in particular typedef F = int Function(int) doesn't create a new type F, it creates the alias F which stands for the already existing type int Function(int)).

But we have discussed similar ideas before. It's not obvious that a proper branding mechanism in the type system would carry its own weight, but we could try to support a similar kind of static discipline outside the type system (for example, #57828, which could serve as a plug-in framework for many different kinds of static checking, with branded types as a starting point).

@kevin-haynie
Copy link

We really need this ability to associate values with enums. There are many times when I need to obtain a list of keys or a list of values from an enum. For example:

enum eLanguage {
  spanish = 'ES',
  english = 'EN',
}

For my UI, I would want to use eLanguage.keys as my dropdown items. For my supportedLocales in my MaterialApp, I would use eLanguage.values.

If I am forced to use static constants, I would have to create 'values' and 'keys' properties in my class and work hard to keep my actual enums and the lists returned from these properties in sync with the actual enums defined. This is a poor solution.

@nateshmbhat
Copy link

Please add this feature. Enum values are very useful

@maerlynflagg
Copy link

We also use enums in different ways (backend and frontend):

enum TestEnum {
X,
Y
}

enum TestEnum2 {
X = 2,
Y = 4,
Z = 8,,
}

enum TestEnum3 {
X = 'someText'
Y = 'someText'
}

it's very useful to add this feature in dart.

@olavemil
Copy link

olavemil commented Mar 4, 2020

While not ideal, couldn't you now technically solve this with extension methods?

enum Flag {
  none, first, second, third
}

extension FlagExtension on Flag {
  String get name => this.toString().substring(this.toString().lastIndexOf('.'));

  int get value {
    switch(this) {
      case Flag.first: return 1;
      case Flag.second: return 2;
      case Flag.third: return 4;
      case Flag.none:
      default: return 0;
    }
  }

  String get key {
    switch(this) {
      case Flag.none: return "0th";
      case Flag.first: return "1st";
      case Flag.second: return "2nd";
      case Flag.third: return "3rd";
      default: return null;
    }
  }

  bool operator <(Flag other) => this.value < other.value;
  bool operator >(Flag other) => this.value > other.value;
  bool operator <=(Flag other) => this.value <= other.value;
  bool operator >=(Flag other) => this.value >= other.value;

  Flag operator &(Flag other) {
    return Flag.values.firstWhere((f) => f.value == (value & other.value));
  }

  int operator |(Flag other) => value & other.value;

  bool isSetFor(int flag) => (this.value & flag) != null;
}

void test() {
  Flag a = Flag.first;
  Flag b = Flag.second;
  print(a.name);

  if (a & b > Flag.none) {
    //do stuff
  }
}

@tomasbaran
Copy link

tomasbaran commented Mar 9, 2020

You could use Maps for that (the only limitation would be not having them as constants). But from my understanding, that's what Maps are exactly for.

Ordered List with values: List;
Unordered List with values: Map;
Unordered List without values: Enum;

So, in your case, it would be:

Map<String,dynamic> role = {
'NormalUser': 1,
'Admin': 2,
'Fiance': 4,
'Bob': 'Fiance & Admin',
};

@LasseRosenow
Copy link

LasseRosenow commented Mar 9, 2020

The problem is, that maps are accessed by keys which are not type safe in the way enums are. Also autocompletion and so on is very bad.

@maerlynflagg
Copy link

enum BackendEnum {
  E1 = 0,
  E2 = 1,,
  E3 = 2,
  E4 = 4,
  E5 = 8,
  E6 = 16,
}

enum MyAppEnum {
  E1,
  E2,
  E3,
  E4,
  E5,
  E6,
}

only dart can't solve this like TypeScript. so currently i used extensions for this:

extension MyAppEnumExtension on MyAppEnum {

  int get serverValue {
    switch (this) {
      case MyAppEnum.E1:
        return 0;
      case MyAppEnum.E2:
        return 1;
      case MyAppEnum.E3:
        return 2;
      case MyAppEnum.E4:
        return 4;
      case MyAppEnum.E5:
        return 8;
      case MyAppEnum.E6:
        return 16;
      default:
        return 0;
    }
  }

  MyAppEnum toAppValue(int serverValue) {
    switch (value) {
      case 0:
        return MyAppEnum.E1;
      case 1:
        return MyAppEnum.E2;
      case 2:
        return MyAppEnum.E3;
      case 4:
        return MyAppEnum.E4;
      case 8:
        return MyAppEnum.E5;
      case 16:
        return MyAppEnum.E6;
      default:
        return MyAppEnum.E1;
    }
  }
}

in this case the getter is nice:

var serverValue = myEnumValue.serverValue;

i'm not like the toAppValue function, because i must to this:

var appValue = MyAppEnum.E1.toAppValue(serverValue);

but i hoped, i can do something like this:

var appValue = MyAppEnum.toAppValue(serverValue);

or do you know a better way?

@lrhn
Copy link
Member

lrhn commented Mar 10, 2020

You can do:

extension MyAppEnumFromValue on int {
  MyAppEnum toAppValue() => MyAppEnum.toAppValue(this);
}

(Your MyAppEnum.toAppValue function needs a return null; at the end.)

@leonluc-dev
Copy link

leonluc-dev commented Apr 10, 2020

As @izayl already stated while class constants work fine for simple comparison/switch scenarios, it's not a solution for type safety, casting and bit wise value handling.

I recently encountered the casting issue during parsing.
Take for example the following (very simplified) piece of Json { mode: 3 } where mode is representing a 'enum value' between -2 and 3. In languages like Swift and C# I could do something similar to this to deserialize to/serialize from a type safe representation of said enum:

public enum MyModeEnum { MinusTwo = -2, MinusOne = -1, Two = 2, Three = 3 }
class MyDeserializedJson
{
     MyModeEnum mode;
}

Deserializing it with most json deserializer libraries would map the integer to the proper enum value. And doing it manually would require a simple int->MyModeEnum cast (or equivalent).

While using extension methods and such for working around the current limitations of Dart is possible, it's a lot of potential code clutter.

@melsheikh92
Copy link

While not ideal, couldn't you now technically solve this with extension methods?

enum Flag {
  none, first, second, third
}

extension FlagExtension on Flag {
  String get name => this.toString().substring(this.toString().lastIndexOf('.'));

  int get value {
    switch(this) {
      case Flag.first: return 1;
      case Flag.second: return 2;
      case Flag.third: return 4;
      case Flag.none:
      default: return 0;
    }
  }

  String get key {
    switch(this) {
      case Flag.none: return "0th";
      case Flag.first: return "1st";
      case Flag.second: return "2nd";
      case Flag.third: return "3rd";
      default: return null;
    }
  }

  bool operator <(Flag other) => this.value < other.value;
  bool operator >(Flag other) => this.value > other.value;
  bool operator <=(Flag other) => this.value <= other.value;
  bool operator >=(Flag other) => this.value >= other.value;

  Flag operator &(Flag other) {
    return Flag.values.firstWhere((f) => f.value == (value & other.value));
  }

  int operator |(Flag other) => value & other.value;

  bool isSetFor(int flag) => (this.value & flag) != null;
}

void test() {
  Flag a = Flag.first;
  Flag b = Flag.second;
  print(a.name);

  if (a & b > Flag.none) {
    //do stuff
  }
}

this is how I use it to mimic Swift enums

@CKKwan
Copy link

CKKwan commented Jun 13, 2020

IMHO, those who comes from C++ / C# will appreciate how important and flexible to be able to associate an Enum with Int.

No, not a class / dictionary / maps representation of enums. If we wish to remain using class / dictionary / maps, we can still do it that way, but there is a reason why enum exist.

For the long term growth of dart, please consider it seriously!

Enum is one of the horrible implementation keeps me away from Java. Besides the pathetic type eraser in Generics.

@Abion47
Copy link

Abion47 commented Jul 17, 2020

Unlikely to be "near future". This has been requested for a while, at least since #21966.

What you are asking for here is really just integer constants and a type alias for them. The constants we can already do:

class Role {
   static const NormalUser = 1;
   static const Admin = 2;
   static const Fiance = 4;
   static const Bob = Fiance & Admin;
}

This is not an ideal solution as it loses the static analysis benefits you get with enums. (e.g. you won't get a warning in a switch if you don't implement a case for every constant)

@slavap
Copy link

slavap commented Jul 17, 2020

With extension it is even not so verbose :-)

enum Role { NormalUser, Admin, Fiance, Bob }

extension RoleEx on Role {
  int get value {
    switch (this) {
      case Role.NormalUser:
        return 1;
      case Role.Admin:
        return 2;
      case Role.Fiance:
        return 4;
      case Role.Bob:
        return Role.Fiance.value & Role.Admin.value;
    }
    return null;
  }
}

And all case statements of switch are generated by IDE, so you don't have to type them.

Or another way, though no linter warning in case of not all enum values are populated in _map:

enum Role { NormalUser, Admin, Fiance, Bob }

extension RoleEx on Role {
  static final Map<Role, int> _map = {
    Role.NormalUser: 1,
    Role.Admin: 2,
    Role.Fiance: 4,
    Role.Bob: _map[Role.Fiance] & _map[Role.Admin]
  };

  int get value => _map[this];
}

@Abion47
Copy link

Abion47 commented Jul 17, 2020

@slavap

If it were implemented natively without extensions, it would look like this:

enum Role { NormalUser = 1, Admin = 2, Fiance = 4, Bob = Fiance & Admin }

One line (max 5 effective) against your 12+. It's also aggravating to have to define code related to an enum outside of that enum's declaration.

And strictly speaking, if this were implemented similarly to how C# handles enums, Bob wouldn't even need to exist as an explicit value, but could instead be dynamically declared as a combination of two other enums:

enum Role { NormalUser = 1, Admin = 2, Fiance = 4 }
const Role Bob = Fiance & Admin;

Also, what if you also need to convert it the other way? Your example would become

num Role { NormalUser, Admin, Fiance, Bob }

extension RoleEx on Role {
  int get value {
    switch (this) {
      case Role.NormalUser:
        return 1;
      case Role.Admin:
        return 2;
      case Role.Fiance:
        return 4;
      case Role.Bob:
        return Role.Fiance.value & Role.Admin.value;
    }
    return null;
  }
}

Role getRoleFromValue(int value) {
  switch (value) {
    case 1:
      return Role.NormalUser;
    case 2:
      return Role.Admin;
    case 4:
      return Role.Fiance;
    case 2 & 4: // equals 6, Very ugly
      return Role.Bob;
  }

  return null;
}

The 12+ lines have now become 24+. Not only that, but the parse value for Bob has to be a hard-coded bitwise-and of the parse values of Admin and Fiance, which is a very ugly solution and not scalable whatsoever. (What if you later get Frank which is a combination of Admin and NormalUser?) Also, how would you support the dynamic combination of two enums as in the example above using this approach? (Spoiler: You can't.)

Then imagine you have to do this boilerplate 5 times, 10 times, 20 times, or 100 times in order to support all the defined enums for an API. What seems "not so verbose" gets tedious and annoying very fast.

@slavap
Copy link

slavap commented Jul 17, 2020

@Abion47 I'm not saying it is ideal, and I prefer to have normal enum support in a language as well. But it is not a stopper currently.
Also 24+ lines are not needed to implement other way conversion:

enum Role { NormalUser, Admin, Fiance, Bob }

extension RoleEx on Role {
  int get value {
    switch (this) {
      case Role.NormalUser:
        return 1;
      case Role.Admin:
        return 2;
      case Role.Fiance:
        return 4;
      case Role.Bob:
        return Role.Fiance.value & Role.Admin.value;
    }
    return null;
  }

  Role roleByValue(int a) => Role.values.firstWhere((e) => e.value == a, orElse: () => null);
}

OR:

enum Role { NormalUser, Admin, Fiance, Bob }

extension RoleEx on Role {
  static final Map<Role, int> _map = {
    Role.NormalUser: 1,
    Role.Admin: 2,
    Role.Fiance: 4,
    Role.Bob: _map[Role.Fiance] & _map[Role.Admin]
  };

  int get value => _map[this];

  Role roleByValue(int a) => _map.entries.firstWhere((e) => e.value == a, orElse: () => null)?.key;
}

Not sure what that means: "how would you support the dynamic combination of two enums as in the example above using this approach? (Spoiler: You can't.)" ?

@Abion47
Copy link

Abion47 commented Jul 18, 2020

@slavap

I'm not saying it is ideal, and I prefer to have normal enum support in a language as well. But it is not a stopper currently.

The point is that it is a stopper for anything much more complicated than a trivial use case. (See below)

Role roleByValue(int a) => Role.values.firstWhere((e) => e.value == a, orElse: () => null);

This method can't go in the extension since doing so would then require an instance of Role in order to call it. It needs to be a static method or a standalone utility method. (Hence my frustration about code relevant to the enum having to be declared outside the enum.)

Not sure what that means: "how would you support the dynamic combination of two enums as in the example above using this approach? (Spoiler: You can't.)" ?

Say you had the system where any user had any one of these roles or a combination of them (which is the whole purpose of a bit flag, which itself is the whole point is this thread), and you had a user that was a NormalUser and a Fiance. Using this approach, there's no way to represent this with Role since it doesn't have an explicit value for it. You would have to then explicitly define not just each individual value but also all combinations of values, the number of which would increase exponentially for every new base value that gets added to Role. A complete example would look like this:

enum Role {
  // base values
  NormalUser, // 1
  Admin, // 2
  Fiance, // 4
  // combination values
  None, // 0
  NormalUserAndAdmin, // 1 | 2 = 3
  NormalUserAndFiance, // 1 | 4 = 5
  AdminAndFiance, // 2 | 4 = 6 (a.k.a Bob)
  NormalUserAndAdminAndFiance, // 1 | 2 | 4 = 7
}

(Switching & to be | here because I just realized we've been doing that declaration wrong for this entire thread. Oops. :P )

Now we have the 3 base values but also the 5 combination values (including None which represents none of the values). Not only is it tiresome to manually type all of these combinations out, but it also makes supporting Role even harder since that's 5 more cases you have to implement on every switch. It's also a cognitive issue because even though AdminAndFiance thematically implies that it is a combination of Admin and Fiance, there isn't actually any real connection between them since they represent completely separate values.

extension RoleEx on Role {
  int get value {
    switch (this) {
      case Role.NormalUser:
        return 1;
      case Role.Admin:
        return 2;
      case Role.NormalUserAndAdmin:
        return 3;
      case Role.Fiance:
        return 4;
      case Role.NormalUserAndFiance:
        return 5;
      case Role.AdminAndFiance:
        return 6;
      case Role.NormalUserAndAdminAndFiance:
        return 7;
    }
    return null;
  }

  Role roleByValue(int a) => Role.values.firstWhere((e) => e.value == a, orElse: () => null);
}

In order to add somewhat of an explicit link between base values and combination values, it requires still more extension methods:

extension RoleEx on Role {
  ...

  bool get isNormalUser => this.value & Role.NormalUser.value > 0;
  bool get isAdmin => this.value & Role.Admin.value > 0;
  bool get isFiance => this.value & Role.Fiance.value > 0;
}

This solution is not scalable. Every nth new base value added to Role increases the number of combination values by a factor of 2. That increases the number of case blocks to include in every switch, the number of isX getter extension methods, and just adds an increasing amount of technical debt. (Not to mention it quickly gets unmanageable. For example, an enum with 10 base values would have to have 2^10 or 1,024 total values to support every possible combination of base values, and you need to declare a case in the extension method for every single one!)

On the other hand, native enum bit flag enums would just work:

enum Role { NormalUser = 1, Admin = 2, Fiance = 4 }

// From int (options)
Role r = 1 as Role; // cast option
Role r = Role(1); // constructor option

// To int (options)
int i = Role.Admin as int; // cast option
int i = Role.Admin.value; // getter option

// Dynamic combination
Role r = Role.NormalUser | Role.Fiance;

// Is NormalUser (options)
if (r & Role.NormalUser > 0) { // explicit bit-wise AND option
  ...
}
if (r.contains(Role.NormalUser)) { // helper `contains` method option
  ...
}

No boilerplate, no complicated case statements, everything just works.

Yes, workarounds exist, but they are either way too verbose to scale well, break static analysis features, or require bending the language over your knee in order to get them to function properly. At this point, the best workaround would probably be to do away with enums entirely and just make a BitFlag class:

(there's probably a cleaner way to implement this)

abstract class BitFlag<T> {
  final String name;
  final int value;
  
  const BitFlag(this.name, this.value);
  
  List<T> get values;
  T create(int value);
  
  T get sumValue => create(values.cast<BitFlag>().fold(0, (prev, v) => prev | v.value));
  
  bool operator ==(dynamic other) {
    if (other is! BitFlag) return false;
    return value == other.value;
  }
  
  T operator +(Object other) => this | other;
  T operator |(Object other) {
    if (other is BitFlag) return (create(value + other.value) as BitFlag).trim();
    if (other is int) return (create(value + other) as BitFlag).trim();
    throw ArgumentError();
  }
  
  T operator &(Object other) {
    if (other is BitFlag) return (create(value & other.value) as BitFlag).trim();
    if (other is int) return (create(value & other) as BitFlag).trim();
    throw ArgumentError();
  }
  
  T operator ~() => (create(~value) as BitFlag).trim();
  
  bool contains(dynamic other) {
    if (other is BitFlag) return this.value & other.value > 0;
    if (other is int) return this.value & other > 0;
    return false;
  }
  
  T trim() {
    return create(value & (sumValue as BitFlag).value);
  }
  
  @override
  int get hashCode => value.hashCode;
  
  @override
  String toString() {
    if (name != null && name.isNotEmpty) {
      return name;
    }
    final sb = StringBuffer();
    for (var v in values) {
      if (contains(v)) {
        if (sb.isNotEmpty) sb.write(', ');
        sb.write((v as BitFlag).name);
      }
    }
    if (sb.isEmpty) return '<Empty>';
    return sb.toString();
  }
}

class Role extends BitFlag<Role> {
  static const NormalUser = Role.value('NormalUser', 1);
  static const Admin = Role.value('Admin', 2);
  static const Fiance = Role.value('Fiance', 4);
  
  static const _values = [NormalUser, Admin, Fiance];
  List<Role> get values => _values;
  
  static const None = Role.value(null, 0);
  static final All = NormalUser | Admin | Fiance;
  
  Role(Role role) : super(role.name, role.value);
  const Role.value(String name, int value) : super(name, value);
  Role create(int value) {
    for (var v in values) {
      if (v.value == value) {
        return v;
      }
    }
    return Role.value(null, value);
  }
}
void main() {
  final bob = Role.Admin + Role.Fiance;
  
  print(bob); // Prints: Admin, Fiance
  print(~bob); // Prints: NormalUser
  print(bob & Role.Admin); // Prints: Admin
  print(bob & Role.NormalUser); // Prints: <Empty>
  print(bob | Role.NormalUser); // Prints: NormalUser, Admin, Fiance
  print(bob.contains(Role.Admin)); // Prints: true
  print(bob.contains(Role.NormalUser)); // Prints: false
  print((bob | Role.NormalUser).contains(Role.NormalUser)); // Prints: true
  
  print(Role.All); // Prints: NormalUser, Admin, Fiance
  print(~Role.None); // Prints: NormalUser, Admin, Fiance
  print(Role.All == ~Role.None); // Prints: true
}

You get most if not all the functionality you would expect from a bit flag enum with this class. But again, you lose out on static analysis this way, and it's still more verbose than should be necessary.

@slavap
Copy link

slavap commented Jul 18, 2020

@Abion47
IMO this:
// Dynamic combination
Role r = Role.NormalUser | Role.Fiance;

Clearly means you don't need an enum, but class with overridden operators, which your example is exactly demonstrating.
E.g. in Java there is no "bit flag enum" and nobody cares much about it.

@Abion47
Copy link

Abion47 commented Jul 18, 2020

@slavap As someone who programmed in Java for years, bit flag enums was one of the many features Java didn't support "because no one cared about it" that ultimately made me dump it for C# and never look back. To be honest, looking back at my time as a Java developer, I learned a lot of bad habits from following "best Java practices" and was thoroughly convinced by many common Java idioms that in retrospect made no sense whatsoever. (Seriously, why the hell, after all these years, does Java still not have language-level support for unsigned integral types?)

Clearly means you don't need an enum, but class with overridden operators, which your example is exactly demonstrating.

The whole point of my example was that a class with overridden operators gets most of the way there but still falls short of the convenience, conciseness, and tooling support that language-level support for bit-flag-enabled enums would provide. For example, a language-level bit flag enum could offer seamless interoperability with the int type itself, eliminating the need for explicit casts and different logic targeting either the int type or the BitFlag type. (De)Serialization of enums becomes a first-class feature of the language since integers can just be as SomeBitFlagTypeed (or .as<SomeBitFlagType>()ed, at least) rather than having to define the conversion methods manually. It would also be a boon to Dart FFI for interoperability with hundreds (if not thousands) of C/C++ libraries that utilize bit flags in precisely this way (currently those C-side enums would have to be converted to ints in order for Dart to use them since there's no good way to parse those bit flag enums into a Dart-equivalent data type).

Sure, you can do a lot of stuff without specifically needing language-level support for it, but that by itself isn't an argument against adding something. By that logic, we shouldn't implement data classes or algebraic data types either because we can just work around the lack of language-level support with custom classes. And why bother implementing nullable types when its just so easy to check for null? For that matter, why even implement extension methods when you can just write a static utility method? Why bother with generics or static typing when you can just use dynamic for everything? (\s, though some people really do argue that last one)

@slavap
Copy link

slavap commented Jul 18, 2020

@Abion47 It is all question of priorities. "nullable types" and extension methods definitely are higher priority than "bit enums".
Also I personally don't like much languages overloaded by excessive syntax sugar, like C#, Kotlin, or C++ 17/20.

@Abion47
Copy link

Abion47 commented Jul 18, 2020

@slavap Bit-flag enum isn't the feature just by itself. It can easily be implemented as part of the much broader feature of value-typed enums which is the fifth most requested feature of all open issues right now, only behind:

1st place: Data classes
(A greatly appreciated feature, and hopefully it can be implemented alongside ADTs which are the 8th place request)

2nd place: Nullable types
(Also a nice feature that is already being implemented)

3rd place: Make semicolons optional
(Not as game-changing a feature and a pretty polarizing one, as long as there's an option in dartanalyzer to make semicolons mandatory/forbidden then I don't care much either way)

4th place: Multiple return types
(Not a great feature by itself, but hopefully it eventually gets implemented as part of a larger feature which adds support for implicit tuple types and tuple deconstruction)

Also of note, 7th place: Pattern matching
(This would couple brilliantly with bit-flag enums since you can define a switch case that matches on if a flag exists in a particular value rather than having it to match exactly)

Also I personally don't like much languages overloaded by excessive syntax sugar, like C#, Kotlin, or C++ 17/20

It's your opinion, though my personal response to that would just be "your loss" since much of that "excessive syntax sugar" leads to incredible increases in productivity once you figure out how to effectively use them. (Kotlin in particular made me seriously consider going back into the Java world for a while.)

@slavap
Copy link

slavap commented Jul 18, 2020

@Abion47 IMO "excessive syntax sugar" leads to incredible unreadable code, especially in case of large development team. You have to enforce more or less unified code style or code review and support will quickly become a nightmare. Look at Golang - simple language without much unnecessary sugar.

@nateshmbhat
Copy link

@Abion47

Agreed 💯 💯 💯 on all the points. We really need this feature and having this will help a huge range of use cases in a much efficient way both in terms of code and execution.

@Abion47
Copy link

Abion47 commented Jul 18, 2020

@slavap There is certainly something to be said about keeping things simple and straight-forward, but there's also something to be said about using the right tool for the job. The syntactic sugar tools make life much easier, but just like literally anything else in the programming world, they can be abused. It's not up to us to say whether a tool is good or bad. It's our job as developers to learn not just the right way to use them, but also the wrong ways. There are times that I appreciate Kotlin's expressiveness and conciseness, and there are times I appreciate Go's no-frills to-the-point approach. I rarely want to exist exclusively in one world or the other. Just like the eternal debate between imperative programming, object-oriented programing, and functional programming, why can't the answer simply be "use the best from all of the above"?

@chiradipmukherjee
Copy link

@Abion47 IMO "excessive syntax sugar" leads to incredible unreadable code, especially in case of large development team. You have to enforce more or less unified code style or code review and support will quickly become a nightmare. Look at Golang - simple language without much unnecessary sugar.

Being able to assign a value against an Enumerated variable is not syntactical sugar at all.

@hy-net
Copy link

hy-net commented Oct 16, 2020

enum is NOT syntax sugar.

enum is known at compile time, but class is only know at run time.

@JustoSenka
Copy link

@slavap

@Abion47 IMO "excessive syntax sugar" leads to incredible unreadable code, especially in case of large development team. You have to enforce more or less unified code style or code review and support will quickly become a nightmare. Look at Golang - simple language without much unnecessary sugar.

Being able to assign a value against an Enumerated variable will make code nice and small.
The need to write extension methods and mapping from const class to int will make incredible unreadable code, not the syntactic sugar.

Java lacked this with enums, while C# improved on it. It seems Dart is going the Java way. I wouldn't agree with that, I believe any mature language should be able to have a flag type enum type, so instead of 50 lines of extension methods, you'd have 10 lines of enum values. Easy and readable, less error prone, compile time error handling, etc.

@ANTONBORODA
Copy link

ANTONBORODA commented Feb 4, 2021

@Abion47
I'm very grateful for the BitFlag class you provided. I took liberty and improved it a little bit to behave a little more closely to C# counterpart

abstract class BitFlag<T> {
  final String name;
  final int value;

  const BitFlag(this.name, this.value);

  List<T> get values;
  T create(int value);

  T get sumValue => create(values.cast<BitFlag>().fold(0, (prev, v) => prev | v.value));

  bool operator ==(dynamic other) {
    if (other is! BitFlag) return false;
    return value == other.value;
  }

  T operator +(Object other) => this | other;
  T operator |(Object other) {
    if (other is BitFlag) return (create(value | other.value) as BitFlag).trim();
    if (other is int) return (create(value | other) as BitFlag).trim();
    throw ArgumentError();
  }

  T operator &(Object other) {
    if (other is BitFlag) return (create(value & other.value) as BitFlag).trim();
    if (other is int) return (create(value & other) as BitFlag).trim();
    throw ArgumentError();
  }

  T operator ~() => (create(~value) as BitFlag).trim();

  bool contains(dynamic other) {
    if (other is BitFlag) return this.value & other.value == other.value;
    if (other is int) return this.value & other == other;
    return false;
  }

  T trim() {
    return create(value & (sumValue as BitFlag).value);
  }

  @override
  int get hashCode => value.hashCode;

  @override
  String toString() {
    if (name != null && name.isNotEmpty) {
      return name;
    }
    final sb = StringBuffer();
    for (var v in values) {
      if (contains(v)) {
        if (sb.isNotEmpty) sb.write(', ');
        sb.write((v as BitFlag).name);
      }
    }
    if (sb.isEmpty) return '<Empty>';
    return sb.toString();
  }
}

The main improvement is that you can now defined a flag that turns other flags on, i.e. you can can define a value like 1 << 2 and other one like (1 << 3) | (1 << 2) and this will set both flags on, also the contains() method is improved to account that change.

@mit-mit
Copy link
Member

mit-mit commented Mar 11, 2021

I'm going to close this issue, not because it isn't important (we know it is!), but because it's being tracked over here in the main language design repo: dart-lang/language#158

Please case votes on that issue. We're thinking about this in combination with a larger theme around structured data (see dart-lang/language#546).

@mit-mit mit-mit closed this as completed Mar 11, 2021
@eernstg
Copy link
Member

eernstg commented Mar 11, 2021

language design repo: #33698

I think that's dart-lang/language#158.

@mit-mit
Copy link
Member

mit-mit commented Mar 11, 2021

Thanks Erik, fixed

@rshillington
Copy link

rshillington commented Mar 1, 2022

@Abion47

(Switching & to be | here because I just realized we've been doing that declaration wrong for this entire thread. Oops. :P )

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests