Open rami-a opened 5 years ago
This would be especially nice in collections:
const supportedDirections = <CompassPoint>{.north, .east, .west};
bool isSupported = supportedDirections.containsAll({.north, .east});
It's worth noting too that we would only allow it in places where we can infer the enum type.
So
final north = .north; // Invalid.
final CompassPoint north = .north; // Valid.
final north = CompassPoint.north; // Valid.
In Swift this feature works not only for enums but also for static properties of classes. See also:
https://github.com/munificent/ui-as-code/issues/7
class Fruit {
static var apple = Fruit(name: "apple");
static var banana = Fruit(name: "banana");
var name: String;
init(name: String) {
self.name = name;
}
}
func printFruit(fruit: Fruit) {
print(fruit.name);
}
// .banana is here inferred as Fruit.banana
printFruit(fruit: .banana);
How would the resolution work?
If I write .north
, then the compiler has to look for all enums that are available (say, any where the name of the enum resolves to the enum class), and if it finds exactly one such which has a north
element, use that.
If there is more than one enum class in scope with a north
element, it's a compile-time error. If there is zero, it is a compile-time error.
If we have a context type, we can use that as a conflict resolution: <CompassPoint>[.north, .south]
would prefer CompassPoint.north
, CompassPoint,south
over any other enum with a north
or south
element.
We won't always have a context type, the example if (myValue == .north) {
does not.
Alternatively, we could only allow the short syntax when there is a useful context type.
For the equality, you will have to write CompassPoint.north
(unless we introduce something like "context type hints" because we know that if one operand of an ==
operator is a CompassPoint
enum type, and enums don't override Object.==
, then the other is probably also a CompassPoint
, but that's a different can of worms).
Then we could extend the behavior to any static constant value of the type it's embedded in.
That is, if you have Foo x = .bar;
then we check whether Foo
has a static constant variable named bar
of type Foo
, and if so, we use it. That way, a user-written enum class gets the same affordances as a language enum
.
I guess we can do that for the non-context type version too, effectively treating any self-typed static constant variable as a potential target for .id
.
(Even more alternatively, we can omit the .
and just write north
. If that name is not in scope, and it's not defined on the interface of this
. then we do "magical constant lookup" for enum or enum-like constant declarations in scope.
That's a little more dangerous because it might happen by accident.
One approach that could be used to avoid writing CompassPoint
several times is a local import (#267).
How would the resolution work?
@lrhn You may want to study how it works in Swift. I think their implementation is fine.
@lrhn
Alternatively, we could only allow the short syntax when there is a useful context type.
If we're taking votes, I vote this ☝️
Regarding the case with if (myValue == .north) {
, if myValue
is dynamic, then I agree, this should not compile. However; myValue
would often already be typed, if it is typed, it should work fine. For example:
void _handleCompassPoint(CompassPoint myValue) {
if (myValue == .north) {
// do something
}
}
For the equality, you will have to write CompassPoint.north
I don't know enough about this, but I don't see why this would need to be the case if we're going with the "useful context type" only route?
Right now we can do:
final direction = CompassPoint.north;
print(direction == CompassPoint.south); // False.
print(direction == CompassPoint.north); // True.
print("foo" == CompassPoint.north); // False.
If we know that direction
is CompassPoint
, can we not translate direction == .south
to direction == CompassPoint.south
? Or is that not how this works?
Even more alternatively, we can omit the . and just write north
I don't personally prefer this approach because we risk collisions with existing in scope variable names. If someone has var foo = 5;
and enum Bar { foo, }
, and they already have a line foo == 5
, we won't know if they mean Bar.foo == 5
or 5 == 5
.
The problem with context types is that operator==
has an argument type of Object
. That gives no useful context type.
We'd have to special case equality with an enum type, so if one operand has an enum type and the other is a shorthand, the shorthand is for an enum value of the other operand's type. That's quite possible, it just doesn't follow from using context types. We have to do something extra for that.
We can generalize the concept of "enum value" to any value or factory.
If you use .foo
with a context type of T, then check whether the class/mixin declaration of T declares a static foo
getter with a type that is a subtype of T. If so, use that as the value.
If you do an invocation on .foo
, that is .foo<...>(...)
, then check if the declaration of T
declares a constructor or static function with a return type which is a subtype of T. If so, invoke that. For constructors, the context type may even apply type arguments.
It still only works when there is a context type. Otherwise, you have to write the name to give context.
To omit the .
would make sense for widgets with constructors.
Text(
'some text',
style: FontStyle(
fontWeight: FontWeight.bold
),
),
Text(
'some text',
style: ( // [FontStyle] omitted
fontWeight: .bold // [FontWeight] omitted
),
),
For enums and widgets without a constructor the .
makes sense to keep, but for widgets where the .
never existed, it makes sense to not add it.
FontWeight.bold -> .bold // class without a constructor
Overflow.visible -> .visible // enum
color: Color(0xFF000000) -> color: (0xFF000000) // class with constructor
_Some pints may have been presented already
padding: .all(10)
This wont work because the type EdgeInsetsGeometry
is expected, but the type EdgeInsets
which is a subclass is given.
textAlign: .cener
This will work because TextAlign
is expected and TextAlign
is given.
The solution for the invalid version would be for flutter to adapt to this constraint.
?.
issueAlot of people have pointed out this issue on reddit. The problem is as follows:
bool boldText = true;
textAlign = boldText ? .bold : .normal;
The compiler could interpret this as boldText?.bold
.
But as mentioned on reddit: https://www.reddit.com/r/FlutterDev/comments/c3prpu/an_option_to_not_write_expected_code_fontweight/ert1nj1?utm_source=share&utm_medium=web2x
This will probably not be a problem because the compiler cares about spaces.
void weight(FontWeight fontWeight) {
// do something
}
weight(.bold);
@ReinBentdal
Omitting the period for constructors would lead to a whole slew of ambiguous situations simply because parentheses by themselves are meant to signify a grouping of expressions. Ignoring that, though, I think removing the period will make the intent of the code far less clear. (I'm not even sure I'd agree that this concise syntax should be available for default constructors, only for named constructors and factories.)
And about the ?.
issue, like I said in both the reddit post and issue #417, the larger issue is not whether the compiler can use whitespace to tell the difference between ?.
and ? .
. It's what the compiler should do when there isn't any whitespace at all between the two symbols. Take this for example:
int value = isTrue?1:2;
Notice how there is no space between the ?
and the 1
. It's ugly, but it's valid Dart code. That means the following also needs to be valid code under the new feature:
textAlign = useBold?.bold:.normal;
And now that there's no space between the ?
and the .
, how should the compiler interpret the ?.
? Is it a null-aware accessor? Is it part of the ternary followed by a type-implicit static accessor? This is an ambiguous situation, so a clear behavior needs to be established.
A solution could be to introduce a identifyer.
*.bold // example symbol
But then again, that might just bloat the code/ language.
I'd like to see something along these lines
final example = MyButton("Press Me!", onTap: () => print("foo"));
final example2 = MyButton("Press Me!",
size: .small, theme: .subtle(), onTap: () => print("foo"));
class MyButton {
MyButton(
this.text, {
@required this.onTap,
this.icon,
this.size = .medium,
this.theme = .standard(),
});
final VoidCallback onTap;
final String text;
final MyButtonSize size;
final MyButtonTheme theme;
final IconData icon;
}
enum MyButtonSize { small, medium, large }
class MyButtonTheme {
MyButtonTheme.primary()
: borderColor = Colors.transparent,
fillColor = Colors.purple,
textColor = Colors.white,
iconColor = Colors.white;
MyButtonTheme.standard()
: borderColor = Colors.transparent,
fillColor = Colors.grey,
textColor = Colors.white,
iconColor = Colors.white;
MyButtonTheme.subtle()
: borderColor = Colors.purple,
fillColor = Colors.transparent,
textColor = Colors.purple,
iconColor = Colors.purple;
final Color borderColor;
final Color fillColor;
final Color textColor;
final Color iconColor;
}
Exhaustive variants and default values are both concepts applicable in a lot of scenarios, and this feature would help in all of them to make the code more readable. I'd love to be able to use this in Flutter!
return Column(
mainAxisSize: .max,
mainAxisAlignment: .end,
crossAxisAlignment: .start,
children: <Widget>[
Text('Hello', textAlign: .justify),
Row(
crossAxisAlignment: .baseline,
textBaseline: .alphabetic,
children: <Widget>[
Container(color: Colors.red),
Align(
alignment: .bottomCenter,
child: Container(color: Colors.green),
),
],
),
],
);
Replying to @mraleph's comment https://github.com/dart-lang/language/issues/1077#issuecomment-688808539 on this issue since this is the canonical one for enum shorthands:
I think this is extremely simple feature to implement - yet it has a very delightful effect, code becomes less repetetive and easier to read (in certain cases).
I agree that it's delightful when it works. Unfortunately, I don't think it's entirely simple to implement. At least two challenges are I know are:
You need a top-down inference context to know what .foo
means, but we often use bottom-up inference based on argument types. So in something like:
f<T>(T t) {}
f(.foo)
We don't know what .foo
means. This probably tractable by saying, "Sure, if there's no concrete inference context type, you can't use the shorthand", but I worry there are other complications related to this that we haven't realized yet. My experience is that basically anything touching name resolution gets complex.
In large part because enums are underpowered in Dart, it's pretty common to turn an enum into an enum-like class so that you can add other members. If this shorthand only works with actual enums, that breaks any existing code that was using the shorthand syntax to access an enum member. I think that would be really painful.
We could try to extend the shorthand to work with enum-like members, but that could get weird. Do we allow it at access any static member defined on the context type? Only static getters whose return type is the surrounding class's type? What if the return type is a subtype?
Or we could make enum types more full-featured so that this transformation isn't needed as often. That's great, but it means the shorthand is tied to a larger feature.
If we extend the shorthand to work with enum-like classes, or make enums more powerful, there's a very good chance you'll have enum or enum-like types that have interesting super- and subtypes. How does the shorthand play with those?
Currently, if I have a function:
foo(int n) {}
I can change the parameter type to accept a wider type:
foo(num n) {}
That's usually not a breaking change, and is a pretty minor, safe thing to do. But if that original parameter was an enum type and people were calling foo
with the shorthand syntax, then widening the parameter type might break the context needed to resolve those shorthands. Ouch.
All of this does not mean that I think a shorthand is intractable or a bad idea. Just that it's more complex than it seems and we'll have to put some real thought into doing it right.
@munificent
If changing the interface breaks the context to the point that name inference breaks, then that is probably a good thing in the same way that making a breaking change in a package should be statically caught by the compiler. It means that the developer needs to update their code to address the breaking change.
To your last example in particular
foo(int n) {}
// to
foo(num n) {}
if that original parameter was an enum type
Enums don't have a superclass type, so I don't really see how an inheritance issue could arise when dealing with enums. With enum-like classes, maybe, but if you have a function that takes an enum-like value of a specific type, changing the type to a wider superclass type seems like it would be an anti-pattern anyway, and regardless would also fall into what I said earlier about implementing breaking changes resulting in errors in the static analysis of your code being a good thing.
Unfortunately, I don't think it's entirely simple to implement. At least two challenges are I know are:
FWIW you list design challenges, not implementation challenges. The feature as I have described it (treat .m
as E.m
if .m
occurs in place where E
is statically expected) is in fact extremely simple to implement. You just treat all occurrences of .m
as a dynamic
, run the whole inference and then at the very end return to .m
shorthands - for each of those look at the context type E
and check if E.m
is assignable to E
(this condition might be tightened to require E.m
to be specifically static final|const E m
). If it is - great, if it is not issue an error. Done. As described it's a feature on the level of complexity of double literals change that we did few years back (double x = 1
is equivalent to double x = 1.0
).
I concede that there might be some design challenges here, but I don't think resolving them should be a blocker for releasing "MVP" version of this feature.
Obviously things like grammar ambiguities would need to be ironed out first: but I am not very ambitions here either, I would be totally fine shipping something that only works in parameter positions, lists and on the right hand side of comparisons - which just side steps known ambiguities.
Just that it's more complex than it seems and we'll have to put some real thought into doing it right.
Sometimes putting too much thought into things does not pay off because you are entering the area of diminishing returns (e.g. your design challenges are the great example of things which I think is not worth even thinking about in the context of this language feature) or worse you are entering analysis paralysis which prevents you from moving ahead and actually making the language more delightful to use with simple changes to it.
That's usually not a breaking change, and is a pretty minor, safe thing to do.
You break anybody doing this:
var x = foo;
x = (int n) { /* ... */ }
Does it mean we should maybe unship static tear-offs? Probably not. Same applies to the shorthand syntax being discussed here.
I'm not a computer scientist but aren't the majority of these issues solved by making it only work with constructors / static fields that share return a type that matches the host class & enum values? That's my only expectation for it anyway, and none of those come through generic types to begin with. If the type is explicit, it seems like the dart tooling would be able to to know what type you're referring to.
I don't think the value of this sugar can be understated. In the context of Flutter it would offer a ton of positive developer experience.
enum FooEnum {
foo,
bar,
baz
}
f(FooEnum t) {}
f(.foo) // tooling sees f(FooEnum .foo)
f(.bar) // tooling sees f(FooEnum .bar)
f(.baz) // tooling sees f(FooEnum .baz)
In the context of Flutter the missing piece that I find first is how to handle foo(Color c)
and trying to do foo(.red)
for Colors.red
. That seems like it would be a nice feature but I'm not sure how you'd handle that quickly and cleanly. I don't think it's necessary to be honest, though.
FWIW you list design challenges, not implementation challenges.
Yes, good point. I mispoke there. :)
As described it's a feature on the level of complexity of double literals change that we did few years back
That feature has caused some problems around inference, too, though, for many of the same reasons. Any time you use the surrounding context to know what an expression means while also using the expression to infer the surrounding context, you risk circularity and ambiguity problems. If we ever try to add overloading, this will be painful.
I concede that there might be some design challenges here, but I don't think resolving them should be a blocker for releasing "MVP" version of this feature.
We have been intensely burned on Dart repeatedly by shipping minimum viable features:
The cascade syntax is a readability nightmare when used in nested contexts. The language team at the time dismissed this as, "Well, users shouldn't nest it." But they do, all the time, and the code is hard to read because of it. No one correctly understands the precedence and god help you if you try to combine it with a conditional operator.
We shipped minimal null-aware operators that were described as a "slam dunk" because of how simple and easy it was. If I recall right, the initial release completely forgot to specify what short-circuiting ??=
does. The ?.
specified no short-circuiting at all which made it painful and confusing to use in method chains. We are laboriously fixing that now with NNBD and we had to bundle that change into NNBD because it's breaking and needs an explicit migration.
The generalized tear-off syntax was basically dead-on-arrival and ended up getting removed.
Likewise, the "minimal" type promotion rules initially added to the language didn't cover many common patterns and we are again fixing that with NNBD (even though most of it is not actually related to NNBD) because doing otherwise is a breaking change.
The crude syntax-driven exhaustiveness checking for switch statements was maybe sufficient when we were happy with any function possibly silently returning null
if it ran past the end without a user realizing but had to be fixed for NNBD.
The somewhat-arbitrary set of expressions that are allowed in const
is a constant friction point and every couple of releases we end up adding a few more cherry-picked operations to be used there because there is no coherent principle controlling what is and is not allowed in a const expression.
The completely arbitrary restriction preventing a method from having both optional positional and optional named parameters causes real pain to users trying to evolve APIs in non-breaking ways.
The deliberate simplifications to the original optional type system—mainly covariant everything, no generic methods, and implicit downcasts—were the wrong choice (though made for arguably good reasons at the time) and had to be fixed with an agonizing migration in Dart 2.
I get what you're saying. I'm not arguing that the language team needs to go meditate on a mountain for ten years before we add a single production to the grammar. But I'm pretty certain we have historically been calibrated to underthink language designs to our detriment.
I'm not proposing that we ship a complex feature, I'm suggesting that we think deeply so that we can ship a good simple feature. There are good complex features (null safety) and bad simple ones (non-shorting ?.
). Thinking less may by necessity give you a simple feature, but there's no guarantee it will give you a good one.
It's entirely OK if we think through something and decide "We're OK with the feature simply not supporting this case." That's fine. What I want to avoid is shipping it and then realizing "Oh shit, we didn't think about that interaction at all." which has historically happened more than I would like.
You break anybody doing this:
var x = foo; x = (int n) { /* ... */ }
Does it mean we should maybe unship static tear-offs?
That's why I said "usually". :) I don't think we should unship that, no. But it does factor into the trade-offs of static tear-offs and it is something API maintainers have to think about. The only reason we have been able to change the signature of constructors in the core libraries, which we have done, is because constructors currently can't be torn off.
@mraleph wrote:
You just treat all occurrences of .m as a dynamic, run the whole inference and then at the very end return to .m shorthands -
Try that with Enum e = .foo..index.toRadixString();
The index
lookup become s dynamic
, which means the toRadixString()
does too, and it won't notice that you forgot the radix argument. If you go back to .foo
, you also need to re-check everything down-stream from there.
(Yes, cascades are great for creating problems with context types, because they allow a context type on a receiver.)
We will have to infer the enum target of .foo
during inference, just as everything else we do type based. That's probably not a problem, because either you have a context type or you don't, and if you don't, you can't use .foo
.
So, for @munificent's design challenges.
Using .foo
requires an enum type as context type. A type variable is not an enum type. Even though enum
declarations are sealed, they have Never
as subtype, so T foo<T extends Enum>() => .foo;
is not valid. Upwards type inference is not a context type, so it does nothing.
For enum-like classes, I'd allow .foo
to match any static getter/constant on the context type (which must be an enum, class or mixin type) which returns a subtype of the context type. (That again disqualifies type variable context types because the getter cannot possibly return a subtype of that). I'd even allow .foo(args)
if foo
is a constructor or static function returning a subtype of the context type.
Yes, changing the argument type of a static function might break code, just as going from double
to num
, or from Function
to Object
, can today. We'll probably live with that, like we already do.
It's not going to be fun explaining why Set<Enum> s = ...; s.contains(.foo);
doesn't work.
I'm more worried about {e? .foo : .bar}
being hard to parse.
Try that with
Enum e = .foo..index.toRadixString();
As I have said before: I would be totally fine shipping something that only works in parameter positions, lists and on the right hand side of comparisons - which just side steps known ambiguities.
Instead of going "lets imagine you can use it everywhere" and then finding tons of problems with that it's fine to say "let just allow it in few places and then maybe expand that further".
@mraleph
As I have said before: I would be totally fine shipping something that only works in parameter positions, lists and on the right hand side of comparisons - which just side steps known ambiguities.
I would not be fine with that. It's too restricted, and not along any clear lines that users can use to understand and remember the limitations. It feels too arbitrary.
A more complete list would probably be to allow .foo
in the following positions:
bar(.foo, name: .foo)
x = .foo
, Enum x = .foo;
e[.foo]
, e[.foo] = 0
[.foo]
, {.foo}
, {.foo: .foo}
==
: e == .foo
- based on static type of e
.(It really won't fly if Enum x = .foo;
works and Enum x = test ? .foo : .bar;
does not. Users going from the former to the latter would be very annoyed at needing to write Enum.
in front for no apparent reason. We'll get requests to fix that before we ever ship the feature - but then we hit the hard grammar issue of text?.foo
meaning something else. ).
That's basically anywhere we allow an <expression>
, plus equality checks.
It's very likely that we won't need binary operators otherwise, class C { operator+(Enum x)=>...} C c = ...; ... c + .foo ...
, although the second operand really is an argument, and we'll likely not want it in a receiver position (mostly because enums have almost no useful properties, and because receivers mostly have no context type anyway).
It really won't fly if
Enum x = .foo;
works andEnum x = test ? .foo : .bar;
does not.
I don't really understand why it has to go all in (especially into complicated territories) to be useful. Yes, users would not be able to write ternaries with shorthands, yes some of them might file feature requests to address this, but if the choice is between (A) not getting the feature at all (because we can't figure out how to resolve grammar ambiguity) and (B) getting it in many places but not in ternaries - I am pretty sure most users would choose (B). What I am advocating here seems to be rather pragmatic.
But in any case I don't think this is my (feature) hill to die on. Ultimately choosing how to scope features is language team's choice, so I trust your collective experience even though I might disagree with the outcome.
A possible compromise could be: allow to replace enum class name with the keyword "enum". E.g. instead of writing
CompassPoint.north
writeenum.north
. It can be resolved according to the rules given by @lrhn in an earlier post:If I write enum.north, then the compiler has to look for all enums that are available, and if it finds exactly one such which has a north element, use that. If there is more than one enum class in scope with a north element, it's a compile-time error. If there is zero, it is a compile-time error.
Not as short as
.north
, but doesn't cause any syntax problems - can be used in any context.
Or maybe #.north
, though I don't know if #
by itself is used for anything right now.
I don't know if
#
by itself is used for anything right now.
It's used by symbol literals. That's another feature that doesn't carry its weight and was arguably under-thought when added to the language.
I wonder how crazy it would be to just use a bare identifier: north
. Collisions and shadowing are possible, so you have to decide which name whens when north
is both an enum and a local variable, inherited member, etc. But we already have a variety of ways that bare identifiers can be resolved and have to prioritize among them, so adding another may not be the worst idea in the world.
It avoids all grammar problems because there is no new syntax. Of course, it exchanges them for name resolution problems...
I don't know if
#
by itself is used for anything right now.It's used by symbol literals. That's another feature that doesn't carry its weight and was arguably undert-hought when added to the language.
I wonder how crazy it would be to just use a bare identifier:
north
. Collisions and shadowing are possible, so you have to decide which name whens whennorth
is both an enum and a local variable, inherited member, etc. But we already have a variety of ways that bare identifiers can be resolved and have to prioritize among them, so adding another may not be the worst idea in the world.It avoids all grammar problems because there is no new syntax. Of course, it exchanges them for name resolution problems...
I would rather not have that option just because it makes it difficult to know what you're looking at. Just looking at foo: .north
vs foo: north
, the former is clear that it's shorthand syntax, whereas the latter is unclear if it's shorthand or some variable or getter available in the current scope.
I didn't even know that Dart had distinct Symbol
literals, or really even of Symbol
as a data type. It sort of makes a certain amount of sense considering Dart's early purpose as a JavaScript alternative, but it's a weird use case and I'm not sure when I would ever use them myself, especially in Flutter.
(The Symbol
class in Dart is unrelated to JavaScript symbols. Dart uses symbols to represent source names at run-time because it then allows AoT compilers, originally dart2js, to change the names in the compiled program without breaking things that refers to those names. That's used for minification and obfuscation. You only really need symbols when you do reflection, including noSuchMethod
and Function.apply
. The design is marred, as @munificent hints, by not defining properly whether #_foo.bar
and #foo._bar
are library private names or not, by allowing you to create symbols at run-time, by not providing a way to create setter-name symbols, and by requiring the constructor to do checking that a const constructor cannot do. And then there is inconsistent use of symbols in dart:mirrors
).
That seems even more than less than unhelpful... and incredibly convoluted.
Using just north
is probably just as viable, technically, as .north
.
Design-wise it's fragile, but no more fragile than any unqualified name referring to an import.
If we simply say that an otherwise unresolved identifier, in an enum type context, is resolved against that enum, then it's slightly magical, but it's similar to having an implicitly imported declaration named north
. Any other import of the same name would make it stop working, and any local declaration with the same name would shadow it. Here it would then refer to the other imported declaration instead of being a conflict, but that will most likely still give you an error.
If we instead say that enums introduce their item names as top-level names too, only those names always lose in name resolution conflicts, then we get a somewhat similar behavior. If any other declaration in the same scope has the same name, we let it take precedence, because you still have Direction.north
as an alternative, And you can always hide the other name.
At least for imported enums, that will give roughly the same behavior as just resolving unqualified names to enums, and it doesn't have to use the context type. It would obviously still have a conflict between two different enum items with the same name, but I don't see that as a common issue.
For enums declared in the same library, we may or may not want to make the top-level enum name shadow imports. If not, it would be like an identifier resolution rule which says even if you found this one, keep searching, and only if you don't find something better, come back and use this declaration.
This would only work for enums, not any enum-like structure. It will only work if the enum declaration is actually imported.
You also need that import to write Direction.north
, but technically you don't need the enum to be imported to write .north
because it's resolved against the context type, not the lexical scope.
(That then prompts the question of why we didn't just make enum items top-level names to begin with, ... and we did, and then we changed that. I do not remember the reasoning, but it could be because it polluted the namespace. Making it a second-class declaration in resolutions might change that balance).
We could use local imports, #267, to make the names of enum values available concisely. This allows us to avoid introducing new, "magic" scope rules. The trade-off is that it does not use context types at all, so we can't use foo(one, one, one)
to mean foo(MyEnum.one, SomeOtherEnum.one, this.one)
, which may a pro or a con depending on the beholder. ;-)
I see three specific approaches mentioned here (one of them only by me, but still ...), so I have tried summarizing them.
We introduce an expression of the form `.' <identifier>
, called an implicit enum expression. This expression must only be used where an enum value "is expected":
case
expressions of a switch
where the switch expression's static type is an enum type.==
/!=
expressions where the other operand is a non-implicit enum expression (and doens't have one in tail-position) with a static type which is an enum type.(An enum type is either a type which implements the type of an enum declaration, which means the type itself, or a type variable bounded by it, or a union type (nullable/FutureOr
) where the component type is an enum type. A type variable is never an enum type, even if its bound is an enum type.)
If the enum type is T
, T?
, FutureOr<T>
, etc., then .foo
is considered equivalent to T.foo
.
The .foo
expression is likely to be a production of <expression>
/<expressionWithoutCascade>
itself. That means we need some special-casing around equality expressions to allow <implicitEnumExpression>
as at most one of the expressions of an ==
/!=
expression, and otherwise only allow it as a production of <expression>
or <expressionWithoutCascade>
.
This can be extended to enum-like types. We can allow .foo
for any context type/switch type/equals type, as long as the type has a static getter named foo
returning something of its own type (for switches, it'll have to be constants).
We can extend to invocations: .foo(args)
is allowed as well, and can resolve to constructor invocations, or to calls of static methods with a suitable return type.
You can write .foo
even if you haven't imported Enum
because it's resolved against the type, not the lexical scope.
Grammar is ambiguous for {e1 ? . foo : e2}
. We'll likely make it mean {e1 ? (.foo) : e2}
because we already prefer the conditional expression in other situations as well, like {e1 is T ? [e2] : e3}
or {e1 as bool ? [e2] : e3}
. It's still one more ambiguity.
Allow a plain identifier foo
to be resolved to Enum.foo
in the same contexts where .foo
would be allowed to work in proposal 1.
Only resolve foo
as an enum value if it is not declared in the lexical scope, in the interface of the static type of this
, or by any applicable extensions (where this.foo
would be an error).
No new syntax.
Can be extended to enum-like classes and function (constructor) calls, since it is based on context type.
Doesn't work if foo
is in scope or in the current class interface, you'll have to write Enum.foo
then (which is not significantly different from having to write this.foo
to get the instance member when foo
is in the lexical scope).
Make enum Enum {foo, bar, baz}
introduce foo
, bar
and baz
as top-level names as well, like a top-level declaration const foo = Enum.foo, bar = Enum.bar, baz = Enum.baz;
next to the enum declaration.
To avoid conflicts with these new names, we introduce a notion of weak declarations (strawman name).
In name conflict resolution, a weak declaration is considered less important than a non-weak declaration. Similarly to how we handle conflicts between platform library declarations and user declarations by ignoring the platform library declarations if there is at least one user declaration, we let weak declarations be less important in conflict resolution than non-weak declarations (unless the non-weak declaration is a platform declaration and the weak declaration isn't).
So, priorities become:
and name conflict resolution succeeds if there is exactly one declaration for a name which has a higher priority than all the rest.
No new syntax, no new way to resolve names, just a different (extended) way to resolve conflicts. (This can still break existing code when a user library enum introduces a name conflicting with an existing top-level platform library declaration, say enum Comparisons {different, equal, identical}
, where identical
would now shadow identical
from dart:core
.)
Only works for enums, not for enum-like classes. They will have to declare their top-level names themselves, but then they can't make those names weak declarations. Unless we introduce weak
declarations in general, but that seems like a bad feature design. Why only two levels? Which proves that it is an ad-hoc solution, not something which is likely to generalize.
Doesn't extend to constructors.
If we do nothing special for enums, we can introduce other scope-controlling features that might be useful here too.
Like @eernstg suggests, a way to locally import the statics of a class namespace into another scope could make it the user's own responsibility to make shorthand names available, or an enum which wants it done in general can export their static constants in a single line (perhaps static export Enum hide values;
)
Proposal 1
Definitely my vote. It's straightforward, it's familiar for people coming from other languages with the feature (e.g. Swift), and it's clear that the identifier is a shorthand reference. And if the only real problem is the syntax ambiguity with ternaries, then embrace the ruling already used with is
and as
and be done with it (especially seeing as interpreting the ?.
as a null-aware operator would cause the code to be invalid once it got to the :
anyway outside of very specific circumstances).
Proposal 2
As convenient as 1 and avoids the ternary ambiguity, but introduces other ambiguities instead. I also don't like how it's not obvious that the identifier is a shorthand which will make visually parsing code using this feature more of a mental exercise than it needs to be.
Proposal 3 (2?)
Same issue as 2 but with the added annoyance of overpopulating the namespace. I can just imagine starting to write a for
loop and Intellisense very helpfully suggesting "Are you trying to type a shorthand for AnimationStatus.forward
?" It only possibly working for enums and not for enum-likes, constructors, or static methods is also a bit of a deal-breaker (although if dart also adopts complex enums then this is less of an issue).
Proposal 4
Same issue as 3 but with the added frustration of having to depend on package authors to be aware of the functionality and implement it for their exported enums. It makes the feature effectively optional which makes it unreliable, and that makes me concerned for its overall adoption and longevity.
There is value in keeping up with expectations from other languages so proposal 1 is my preference and I agree with @Abion47's points above. We could have made the spread operator $*(!@
as far as I know but we didn't because there is an expectation that it would be a certain way due to prior art in other languages.
The {e1 ? . foo : e2}
would not be invalid if ?.
is treated as null-aware member access, it just becomes a map literal instead of a set literal.
Swift uses .foo
. I'm not aware of any other language doing so. Swift has a significantly different syntax than Dart, so it doesn't follow that just because something works for Swift, it works just as well for Dart. We are stuck with a grammar descending from C, so some things are just harder than for, say, Swift or Kotlin.
C# is considering plain identifiers ("target type lookup"), but haven't decided on anything yet AFAICS. I don't think Java/Kotlin have enum value shorthands.
(Ob-pedantry: There are two ternary operators in Dart, ?:
and []=
. The former is the conditional expression operators.)
@lrhn this ambiguity reminds me of the ambiguity problem in NNBD ?[
or ?.[
.
Can this be solved using the same logic that solved the NNBD issue?
.foo
seems the most reasonable approach so far.
I agree that we shouldn't start modeling Dart after other languages, particularly languages like Swift with a significantly different syntax. However, as you say, this specific feature mostly comes from Swift, so people using Dart that have Swift experience would expect the syntax to be similar if not identical, and it would be major cognitive dissonance otherwise.
I didn't know C# was considering plain identifiers. I really hope they don't, for the same reasons I laid out before. It would be very annoying to be given source code with a bunch of identifiers with implicit enum types that I didn't know about and have to piece it all together myself.
It doesn't surprise me that Java doesn't have enum shorthands and probably never will. It would surprise me if the Kotlin design team didn't have it at least somewhere on their backlog.
The {e1 ? . foo : e2} would not be invalid if ?. is treated as null-aware member access, it just becomes a map literal instead of a set literal.
That's what I was referring to when I said "very specific circumstances". :P
Also with Proposal 1, by adding the .
in a field that an enum is expected, the analyzer could just suggest all the enum cases. With Proposal 2 and 3, it would be hard to decide what to suggest and when:
final AutovalidateMode mode = .
When writing the dot, the idle should suggest AutovalidateMode.disabled
, AutovalidateMode.always
, AutovalidateMode.onUserInteraction
final AutovalidateMode mode =
Now this it doesn't display the autosuggest. This would also suggest all defined variables with the type AutovalidateMode.
Autocompleting constant values of the context type should not be a problem.
When the context type is a class/mixin type which declares static variables of its own type, proposing those makes sense, just as proposing constructors do. I wouldn't worry about not being able to give good completions for something which are easily detectable static declarations.
I think the biggest loss would be losing out on style class static members. For example, an argument of type Color
but wanting to access Colors.red
as .red
or AppStyle.primary
as .primary
.
Questions it raises:
extension ColorX on Color {
static final primary = AppStyle.primary;
}
would static programming help here?
[Edit: Changed enum.red
back to Colors.red
, plus similar cases — I misunderstood the code and thought .red
was an enum value.]
I think we should be careful not to pile up too much of a mountain of syntactic ambiguity. We are already handling a lot of complexity in that area, and I'm convinced that it has some connection to readability for humans.
So maybe we should consider approaches that don't introduce any kind of syntactic complexity, e.g.:
E
denotes an enum
declaration containing a value v
then enum.v
is desugared to E.v
when the context type is E
or E?
. This would handle all the context type cases.enum
declaration E
in scope contains the value v
, enum.v
is desugared to E.v
. This would handle cases with no context, but a unique name.We could also allow class.staticMethod()
using the latter approach, but that's hardly readable. Using new<...>.lastPartofConstructorName(..)
is more promising, and it could succeed based on the context type, with a full search for constructors in all classes in scope as a fallback.
Here are the examples from here and here:
final example = MyButton("Press Me!", onTap: () => print("foo"));
final example2 = MyButton("Press Me!",
size: enum.small, theme: enum.subtle(), onTap: () => print("foo"));
class MyButton {
MyButton(
this.text, {
@required this.onTap,
this.icon,
this.size = enum.medium,
this.theme = new.standard(),
});
final VoidCallback onTap;
final String text;
final MyButtonSize size;
final MyButtonTheme theme;
final IconData icon;
}
enum MyButtonSize { small, medium, large }
class MyButtonTheme {
MyButtonTheme.primary()
: borderColor = Colors.transparent,
fillColor = Colors.purple,
textColor = Colors.white,
iconColor = Colors.white;
MyButtonTheme.standard()
: borderColor = Colors.transparent,
fillColor = Colors.grey,
textColor = Colors.white,
iconColor = Colors.white;
MyButtonTheme.subtle()
: borderColor = Colors.purple,
fillColor = Colors.transparent,
textColor = Colors.purple,
iconColor = Colors.purple;
final Color borderColor;
final Color fillColor;
final Color textColor;
final Color iconColor;
}
// -----------------------------------------------------
return Column(
mainAxisSize: enum.max,
mainAxisAlignment: enum.end,
crossAxisAlignment: enum.start,
children: <Widget>[
Text('Hello', textAlign: enum.justify),
Row(
crossAxisAlignment: enum.baseline,
textBaseline: enum.alphabetic,
children: <Widget>[
Container(color: enum.red),
Align(
alignment: enum.bottomCenter,
child: Container(color: enum.green),
),
],
),
],
);
This is just my opinion, but I feel that, even though those examples are syntactically precise, they're semantically ambiguous. enum
is being used as a placeholder for so many different types, and it would be hard for the reader to discern which type it's referring to. For example, if I wanted to change crossAxisAlignment: enum.start
, I'd have to go to Column
's documentation, find the constructor being used, find which type crossAxisAlignment
expects, then search that up, and find a list of values there. All that instead of just searching up the CrossAxisAlignment
enum directy and looking for options there. Plus, there's no way of knowing that Column
's crossAxisAlignment
uses the same type as Row
's. I don't think that kind of ambiguity is worth the few characters we save when typing.
For example, if I wanted to change
crossAxisAlignment: enum.start
, I'd have to go toColumn
's documentation
I guess the same way you go to Columns constructor you could go directly to the property you want to know about.
Also, by using autocomplete you can delete the value after enum.
and it would bring to you the options for that enum.
I liked the @eernstg idea because it is pretty straightforward to identify enums.
The way it works now kinda forces us to know the enum type name, if it wasn't for that we wouldn't need to know the type name at all when consuming the enum. So IMO this proposal makes a lot of sense.
Would be even cooler if it was enabled to use with data classes.
I'm not sure I see what the enum.X
syntax solves that the .X
syntax doesn't, not to mention it seems to suffer from the same technical hurdles. My personal issue with it is also with how many times the word "enum" appears in that example, it starts becoming hard to read everything else. Personally, I'd rather go for the dot syntax route since, if we are talking about syntactic sugar anyway, in this case I'd argue that conciseness surpasses explicitness in this instance.
It also doesn't solve the issue in Flutter where a lot of fields aren't enums but instead class constructors or static factories, and often of a class that is different than the one explicitly expected. For example, You put Container(color: enum.red)
(which I'm assuming you meant color: new.red
or something), which is a good example because the color
field expects a value of type Color
, but people typically use the Colors
class to supply the value (e.g. Colors.red
). The only thing I could see addressing that issue would be to introduce a new keyword for class declarations that make them implicitly compatible with other classes of related types:
// Mark entire class so any static field or function gets included in the implicit type's static field/function list.
class Colors implicit Color {
...
// Mark individual fields as being implicit
static const implicit Color red = ...;
}
I'm sure this has a whole slew of issues though, not least of which that it introduces an entirely new keyword and functionality and complexity to the type system for what effectively amounts to a minor intellisense improvement.
I'm sure this has a whole slew of issues though, not least of which that it introduces an entirely new keyword and functionality and complexity to the type system for what effectively amounts to a minor intellisense improvement.
I'm wondering if this entire issue is better off with being an IDE improvement instead of a language feature. Readability-wise, it's easier to see the whole type of the enum spelled out, and when typing, it's more convenient to type less. Instead of sacrificing one for the other, why not leverage the sweet spot of IDEs as autocomplete tools?
Hey, that's interesting, everybody hates the enum.nameOfValue
idea! ;-)
However, let me comment on some of the worries about it:
@Levi-Lesches wrote:
even though those examples are syntactically precise, they're semantically ambiguous
I don't see how .start
is less semantically ambiguous than enum.start
: The latter actually narrows down the possible meaning by indicating that start
is the name of an enum value, and we're comparing to an approach where .start
could also denote the second half of a constructor name, or a static method.
So it may be bad for readability that enum.start
doesn't syntactically reveal which enum we're talking about, but the basic proposal will insist that this is indicated directly by the context type.
Granted, if we take the more aggressive proposal and also allow enum.start
with no context type (that matters), and search for all enums in scope, this worry is very relevant. That's the main reason why I made them two distinct proposals, and marked the latter as 'aggressive'.
For example, if I wanted to change
crossAxisAlignment: enum.start
, I'd have to go toColumn
's documentation, find the constructor being used, find which typecrossAxisAlignment
expects, then search that up, and find a list of values there.
The token enum
is statically known to denote the relevant enum declaration (that's determined by the context type), so you should get the same level of IDE support as you would have if you had chosen to write the name of the enum. (Of course, it's not that easy in an editor that gets no help from the analyzer, but this is not unique to enum.*
.)
Note that if we include the aggressive 2nd proposal and allow enum.start
even without a context type, you would still have the same connection: Either enum.start
is an error, or it is known to denote a specific value of a specific enum declaration, and you can of course get all the information about that enum by hovering over the text "enum".
So if you think there's an enum value starting with star
and you want to use it, but you have no context type and you don't remember the name of the enum or where it's defined, you would type enum.star
and use completion to select the right value from the right enum, say MyEnum.start
, and it would then complete to enum.start
.
A quick fix that immediately seems natural to me would transform MyEnum.start
into enum.start
and vice versa, such that you can use enum
while writing it up, and switch to MyEnum.start
, assuming that's your preferred style.
@Abion47 wrote:
I'm not sure I see what the
enum.X
syntax solves that the.X
syntax doesn't
The main reason why I'm proposing the former rather than the latter is the following:
The .name
syntax has a very real cost in terms of the available future syntactic design space for Dart. The more ambiguity we cram into the overall grammar of Dart, the more we'll have to jump through hoops (including subtle and perhaps surprising syntactic disambiguation rules that go beyond what is expressible in context free grammars) in order to make future language constructs parseable. I think we should be careful when we're about to enter into a "make an unknown amount of future extensions impossible!" party. The form enum.start
is completely simple and parseable because enum
is a reserved word and 'enum' '.'
is always a syntax error in Dart of today.
So I'm certainly putting the emphasis on a technical detail, but I believe the wider consequences of that detail actually make a difference.
it seems to suffer from the same technical hurdles
So that's one thing that I don't agree on.
hard to read .. conciseness surpasses explicitness
I can understand that preference, I would certainly go for conciseness in a lot of cases. However, in this particular case it isn't for free.
Also, I do tend to like the idea that we fix the "out-of-scope-looked-up" name to a kind by saying that it must be the name of an enum value (especially if it could just as well be a static getter, or the second half of the name of a constructor, etc., if we come up with more).
For example, You put
Container(color: enum.red)
(which I'm assuming you meantcolor: new.red
or something), which is a good example because thecolor
field expects a value of typeColor
, but people typically use theColors
class to supply the value (e.g.Colors.red
).
Ah, I didn't check the meaning of the identifiers at that location in the code carefully enough (and I'm not usually writing Flutter code, so I don't just know these things). Sorry about that.
So what we would like to express here is Container(color: Colors.red)
, and Colors.red
is a static getter of the class Colors
.
This could be abbreviated, but only with the 'aggressive' 2nd part of my proposal: We would need to support a mechanism whereby class.red
would denote the static member C.red
, where C
is selected from all classes in scope, presumably requiring that the basename red
is unique among such members, or at least that it is unique among such members where the type of the member does not give rise to a compile-time error.
I'm rather sceptical about this idea: It would be a very fragile mechanism, and it could easily break if we add, say, another import to the current library (such that the uniqueness doesn't hold any more).
Anyway, we could do it, and we would then use Container(color: class.red)
Note that Container
denotes a constructor, so we could make that new(color: class.red)
, if it is used in a context where the context type is Container
.
it introduces an entirely new keyword and functionality and complexity to the type system
That is not true, enum
(and the others like class
and new
) are reserved words already, which is the reason why it does not introduce any issues during parsing. For the type system it makes no difference at all: The context type is independent of the new syntax, so when we encounter enum.start
or a similar construct, we can just inspect the context type (it's already defined today, no need to change that), and then we check
enum
declaration? If no, error. Otherwise, call it E
.E
contain a value named start
? If no, error. Otherwise desugar enum.start
to E.start
.When this is done the rest of Dart is completely the way it used to be, no extra complexity will arise because of this mechanism.
The same is true for all the others (aggressive or not), they desugar away immediately, and it's not complex.
@eernstg thinking more about the enum.x
and I've found it incredible.
There are too many pros and basically no cons at all.
1) enum is a reserved word - no clashes here
2) easy to type
3) easy to remember
4) no need to see the real enum type as it doesn't bring too much value to the code (the property/variable should be enough to identify what you are doing, if it is not, then IMO it is probably a bad code there)
5) can be used with data types
6) can be optional with a lint: do_not_use_enum_type_desugary
(Also I am definitely against the .value
if it can really harm the language future)
The
.name
syntax has a very real cost in terms of the available future syntactic design space for Dart.
@eernstg Do you have an example of this? Is there any plan to use this kind of syntactic design in other constructs for Dart?
It's much simpler than that: It introduces additional syntactic ambiguity. Every time we do that, we need to introduce some kind of disambiguation, and that's usually relying on properties like "there is no expression that starts with <something>
". So we're in trouble if we introduce a new kind of expression that starts with that <something>
.
@eernstg
That is not true,
enum
(and the others likeclass
andnew
) are reserved words already, which is the reason why it does not introduce any issues during parsing.
I was not referring to your proposed solution in that part of my reply. I was referring to my proposal of implicit
(which I wasn't really serious in proposing) as a workaround to the issue of providing this shorthand syntax support to cases like the Color
<> Colors
issue. I was pointing out that although that issue makes the shorthand syntax unreasonably unwieldy if it has to scan the entire namespace for compatible matches, any solutions to that issue would likely require new mechanics which will only complicate the type system further for relatively little gain.
Note that Container denotes a constructor, so we could make that new(color: class.red), if it is used in a context where the context type is Container.
The vast majority of contexts in Flutter dealing with child widgets specify a type of Widget
, with a few places specifying a specific subset of Widget
. I can't think of any single place that specifically requests a Container
, nor any of the other most common widget types.
My favorite option would be option #2
followed by option #1
. Option #2
just looks cleaner given typical Flutter code with a lot of enums and enum like things. IDE users can always jump to definition to resolve ambiguity. We can also provide quick fixes to help users out for cases where there is a conflict. We could also provide a lint with a quickfix to specify the class name so that packages like Flutter that would probably want to avoid this shorthand could avoid.
Option #2
return Column(
mainAxisSize: max,
mainAxisAlignment: end,
crossAxisAlignment: start,
children: <Widget>[
Text('Hello', textAlign: justify),
Row(
crossAxisAlignment: baseline,
textBaseline: alphabetic,
children: <Widget>[
Container(color: red),
Align(
alignment: bottomCenter,
child: Container(color: green),
),
],
),
],
);
Option #1
return Column(
mainAxisSize: .max,
mainAxisAlignment: .end,
crossAxisAlignment: .start,
children: <Widget>[
Text('Hello', textAlign: .justify),
Row(
crossAxisAlignment: .baseline,
textBaseline: .alphabetic,
children: <Widget>[
Container(color: .red),
Align(
alignment: .bottomCenter,
child: Container(color: .green),
),
],
),
],
);
When using enums in Dart, it can become tedious to have to specify the full enum name every time. Since Dart has the ability to infer the type, it would be nice to allow the use of shorter dot syntax in a similar manner to Swift
The current way to use enums:
The proposed alternative: