-
Notifications
You must be signed in to change notification settings - Fork 53
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
Generate real enums instead of classes with static members #446
Comments
I like this idea! As an alternative, we could wait on extension types dart-lang/language#2727. That should give us the same static typing benefits, but without the runtime overhead of actually allocating an object. cc @eernstg. |
Extension types would be nice in this case and I considered them too but I didn't see anything in that discussion indicating they would support exhaustiveness checking (I don't see why they wouldn't, but it wasn't really discussed there). Given that this is a simple enough rewrite with immediate benefits (and hopefully non-breaking), I would hope it's worth doing if extension types are still a while away. |
It's a breaking change, we might have users putting enum values in ints and vice versa... But we're not really shy of doing breaking changes. Worst case we can make it opt-in at first via a config option. If we create such an enum-like class, I would also advocate for adding an extra |
Just wondering, if an enum-like class is relevant then why not go all in?: enum CanStatus {
ok(1),
socketCreateError(2),
example(1);
final int value;
const CanStatus(this.value);
}
String getStatusName(CanStatus status) {
throw 'NotImplemented';
}
void main() {
print(getStatusName(CanStatus.ok));
} (OK, I renamed the enum-ish values to use Dart style identifiers, I don't know if that's good style in this context.) I modeled enum CanStatus {
ok(1),
socketCreateError(2);
static const example = ok; // Just an alias for an existing value.
final int value;
const CanStatus(this.value);
} |
Oh I was wrong in my initial assessment then: because you can't currently use the enum type, this is necessarily a breaking change almost all the time because a simple
Yeah, the original reason enums have been turned down in the past is because two enum values in C can be identical but not in Dart. I was trying to think of a way to solve this with enhanced enums but I didn't realize it was that simple! |
Here is a variant that shows how this would work with extension types: extension type const CanStatus._(int value) {
static const ok = CanStatus._(1);
static const socketCreateError = CanStatus._(2);
static const example = ok; // With an extension type they must be the same.
}
String getStatusName(CanStatus status) => switch (status) {
CanStatus.ok => "ok",
CanStatus.socketCreateError => "socketCreateError",
/* `CanStatus.example => ...` would be unreachable */
_ => 'Unexpected value',
};
void main() {
print(getStatusName(CanStatus.ok)); // Typed.
print(getStatusName(30 as CanStatus)); // But we can bypass the constructor.
} In this case we only have the One consequence of this is that we cannot distinguish the If the support for enum-like classes is extended to deal with enum-like extension types then we could presumably have a check for covering all the "nicely declared" values in the switch (that is, all the non-rogue values), but we would still have to have a default clause for the potential rogue value. So this is a new notion of being exhaustive where we treat different subsets of the values that we may encounter at run time in different ways (you could say that we partition the set of possible objects and then handle each partition separately, and the declared values (static const in CanStatus) must be covered; other values are handled via |
Thanks @eernstg ! enum CanStatus {
ok(1),
socketCreateError(2),
example(1); @eernstg are
This is what we need. We can't distinguish
Ah, with the cast we can create rogue values. Hm, that makes extension types less nice than generating a class with a private constructor for exhaustiveness checking. Don't we have extension types with private constructors that resist casts? =) |
That's a fundamental property of extension types. An extension type may look a lot like a class with one, final instance variable—like a "wrapper object" that contains another object, which is the "wrapped object"—but there is no wrapper object at run time, and hence there's no protection barrier. In particular, with an actual wrapper class we can check that the value of an instance variable satisfies some constraints (they would typically be checked by the constructor). When the wrapper does not exist at run time, a dynamic type check can only verify that the given object has the type which is the representation type of the claimed extension type, and the dynamic type check has no way to determine whether we did or did not execute an extension type constructor where the wrapped object was vetted. For instance, if we are checking whether So you can introduce some static checks based on extension types, but it's a matter of manual discipline to ensure that we don't use casts in ways that allow us to skip invocations of constructors whose behavior is needed for correctness. Alternatively (as in the case with the enumerated values from C), we may inherently have the potential for rogue values, and then we just have to introduce some code that handles them. |
That is true, the C code could return rogue values, if we want to support exhaustiveness checking, we should check the int value at the moment we get the value from C code (as a return value or field access) by running it through a check. |
Perhaps you could offer both approaches: (1) An FFI type like a C enum is represented by a Dart |
I'm leaning towards this. It would however be nice to override And then throw an If this approach is too strict, we can add a config option later that will generate extension types on int instead (for the enum names that match the filter in the config). @mannprerak2, @liamappelbe, @Levi-Lesches any thoughts, concerns, ideas? @Levi-Lesches If the rest is happy with this approach, would you be willing to contribute a PR? (Note the master branch has recently switched to track Dart dev releases, but we should be able to make this change against the stable branch in this repo, see https://github.com/dart-lang/ffigen/wiki/Branches.) |
LGTM, as this would let me use the |
I'm not sure I understand, I suppose that wouldn't mean We can use an extension type to get this effect, except that extension types can't redefine enum CanStatus {
ok(1),
socketCreateError(2);
static const example = _CanStatusExample(ok);
final int value;
const CanStatus(this.value);
String toString1() => toString();
}
extension type const _CanStatusExample(CanStatus status) implements CanStatus {
String toString1() => 'CanStatus.example';
}
// This could be in some other library.
void main() {
print(CanStatus.ok.toString1()); // 'ok'.
print(CanStatus.example.toString1()); // 'example'.
print(identical(CanStatus.ok, CanStatus.example)); // 'true'.
String doSwitch(CanStatus status) => switch (status) {
CanStatus.ok => 'ok',
CanStatus.socketCreateError => 'socketCreateError',
// OK, this switch is already exhaustive.
};
print(doSwitch(CanStatus.example)); // 'ok'.
} Note that we make We could do something else, however: enum CanStatus {
ok(1),
socketCreateError(2);
static const example = ok; // Just an alias for an existing value.
final int value;
const CanStatus(this.value);
static final _nameMap = values.asNameMap()
..['CanStatus.example'] = CanStatus.example;
static CanStatus? byName(String s) => _nameMap[s];
}
void main() {
print(CanStatus.byName('CanStatus.example') == CanStatus.ok); // 'true'.
} |
Sorry for the ambiguity! 🤓
No, that is exactly what I want. It has two identifiers, and the toString should reflect that.
That is fine, these are aliases of each other, I just want the toString to reflect all aliases instead of just the first one. |
OK, but then it should work just fine with a plain enum (especially if it's generated ;-). enum CanStatus {
ok(1),
socketCreateError(2);
static const example = ok; // Just an alias for an existing value.
final int value;
const CanStatus(this.value);
@override
toString() {
if (this == ok) return 'CanStatus.ok, CanStatus.example';
return super.toString();
}
}
void main() => print(CanStatus.example); |
...It's been a long week. I'll start working on this now. |
Well, that was probably a little simpler than I thought. I spent a few hours on it, here's what I've got. The simple case: enum Colors {
red,
green,
blue,
}; enum Colors {
// ===== Unique members =====
red(0),
green(1),
blue(2);
// ===== Aliases =====
// ===== Constructor =====
final int value;
const Colors(this.value);
// ===== Override toString() for aliases =====
@override
String toString() {
return super.toString();
}
} The complex case: typedef enum CanStatus {
ok = 1,
socketCreateError = 2,
example = 1,
} CanStatus; enum CanStatus {
// ===== Unique members =====
ok(1),
socketCreateError(2);
// ===== Aliases =====
static const example = ok;
// ===== Constructor =====
final int value;
const CanStatus(this.value);
// ===== Override toString() for aliases =====
@override
String toString() {
if (this == ok) return "CanStatus.ok, CanStatus.example";
return super.toString();
}
} |
Here's the current behavior:
I understand that
ffigen
can't make this an actual enum becauseCanStatus.OK == CanStauts.EXAMPLE
, and you can't overrideEnum.operator==
. But the current approach has another problem:The type gets completely erased and the original name of the enum can't be used! You could use
But you can't use the original name, because then
CanStatus.OK
becomesint.OK
, which isn't defined. This approach isn't great either because then Dart thinksstatus = 30
is okay, when that value isn't even defined.Instead, this would be preferable:
That way this would be recognized as an "enum-like class" that would enable using the type
CanStatus
directly and enable exhaustiveness checking for switch statements and expressions, likegetStatusName
above.The text was updated successfully, but these errors were encountered: