-
Notifications
You must be signed in to change notification settings - Fork 211
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
Allow for shorter dot syntax to access enum values #357
Comments
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: 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 If we have a context type, we can use that as a conflict resolution: Alternatively, we could only allow the short syntax when there is a useful context type. 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 (Even more alternatively, we can omit the |
One approach that could be used to avoid writing |
@lrhn You may want to study how it works in Swift. I think their implementation is fine. |
If we're taking votes, I vote this ☝️ Regarding the case with void _handleCompassPoint(CompassPoint myValue) {
if (myValue == .north) {
// do something
}
}
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
I don't personally prefer this approach because we risk collisions with existing in scope variable names. If someone has |
The problem with context types is that 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 It still only works when there is a context type. Otherwise, you have to write the name to give context. |
To omit the FromText(
'some text',
style: FontStyle(
fontWeight: FontWeight.bold
),
), ToText(
'some text',
style: ( // [FontStyle] omitted
fontWeight: .bold // [FontWeight] omitted
),
), For enums and widgets without a constructor the FontWeight.bold -> .bold // class without a constructor
Overflow.visible -> .visible // enum
color: Color(0xFF000000) -> color: (0xFF000000) // class with constructor From issue #417_Some pints may have been presented already Not include subclasses of typeInvalidpadding: .all(10) This wont work because the type ValidtextAlign: .cener This will work because The
|
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
Notice how there is no space between the
And now that there's no space between the |
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 #1077 (comment) on this issue since this is the canonical one for enum shorthands:
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: How does it interact with generics and type inference?You need a top-down inference context to know what f<T>(T t) {}
f(.foo) We don't know what What does it mean for enum-like classes?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. How does it interact with subtyping?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 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. |
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) {}
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. |
FWIW you list design challenges, not implementation challenges. The feature as I have described it (treat 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.
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.
You break anybody doing this:
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.
In the context of Flutter the missing piece that I find first is how to handle |
Yes, good point. I mispoke there. :)
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.
We have been intensely burned on Dart repeatedly by shipping minimum viable features:
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 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.
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. |
Typedefs looks like a viable option, but by itself, it won't help. static extension on AlignmentGeometry {
typedef Absolute = Alignment;
typedef Directional = AlignmentDirectional;
}
AlignmentGeometry a = .Absolute.topRight; How is
What do you mean by "better"? It's shorter, but to be able to say it's better, you have to come up with a theory that explains how this constant makes its way into static extension on AlignmentGeometry {
typedef Absolute = Alignment default;
typedef Directional = AlignmentDirectional;
}
AlignmentGeometry a = $.topRight; where AlignmentGeometry a = #.topRight;
AlignmentGeometry a = `.topRight;
AlignmentGeometry a = `topRight; // no dot
//etc. |
I'm not advocating that every static declaration on a subclass should necessarily be available on the superclass.
Someone put it there. That's the answer every time. |
I don't agree with your characterization of the proposal of copying/pasting/renaming stuff in bulk as a "theory". At least not a "good theory". I tried to list some arguments explaining why it was not a good theory every time it resurfaced, but the only important factor is whether Flutter people will like it or not, and I can bet a proverbial farm on them not liking it passionately enough. :-) |
Keep in mind, this feature making it in will have package maintainers move to improve their apis with this in mind. for |
You have to convince the maintainers to do the following:
Look insideContainer(
decoration: const .box(
border: .border(
top: .new(color: .new(0xFFFFFFFF)),
left: .new(color: .new(0xFFFFFFFF)),
right: .new(),
bottom: .new(),
),
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0),
decoration: const .box(
border: .border(
top: .new(color: .new(0xFFDFDFDF)),
left: .new(color: .new(0xFFDFDFDF)),
right: .new(color: .new(0xFF7F7F7F)),
bottom: .new(color: .new(0xFF7F7F7F)),
),
color: .new(0xFFBFBFBF),
),
child: const Text('OK',
textAlign: .center,
style: .new(color: .new(0xFF000000))),
),
);
} Please be honest: do you like this code? |
If mangling the name results in a bad API, then it's probably not a good default and you shouldn't use it that way anyway. Generally a "default constructor" conflict doesn't exist as the type names are different and can provide a reasonable name. There is also nothing stopping you from namespacing things with records or middleman objects, riverpod style. There is no problem with having the type of a getter or static method as a subtype, and I don't know what you're talking about regarding chaining and grafted members. If you're copy pasting a lot, you're doing something wrong. It's just a single getter, static field, or factory/static method for most cases A change to the API would be purely additive. And of course you need to take that into account for docs, it's an improvement on how defaults are accesses. That code is mediocre because it abuses the feature. There is nothing stating we couldn't have an annotation + lint that says "don't dot-notate this member", or a lint that's specifically about .new |
Suppose you have grafted EdgeInset constructor EdgeInsetGeometry g = EdgeInset.all(10).copyWith(right: 15); // works with full name
EdgeInsetGeometry g1 = .all(10).copyWith(right: 15); // error! The reason for an error is that the "grafted" constructor |
I may have said it before, but with all the long conversations in this thread, I believe there is no one clear rule that is obvious and intuitive for everyone. Maybe this would be better suited as an IDE auto-complete suggestion that transforms eg |
Not necessarily. The static member can be a subtype, which we can act on, so long as the type we ended up with is still valid. How we get there doesn't matter. After all, |
|
I disagree because this solves a lot of problems with source generated stuff. |
Const is not exactly going to help with your copyWith. Pick which is a better default. A factory for const, a static method for subtype context, or, if you don't need arguments, let it be a constant static variable. This issue does not replace the normal way of doing things, it's only a useful shorthand for most usecases. If you need more, go back to the specific one. |
@tatumizer wrote:
It wouldn't be difficult to preserve both: #4153. |
it's been a while people don't talk (fight) about this issue. Flutter has this request which seems like could be used by whatever you do with EdgeInsetGeometry once this is shipped. |
Annotations can't interfere with the semantics of a program. The language can't decide the context type or accept a shorthand based on an annotation. Considering this, this is not a viable alternative. We would have to have something in the language side to do this. The current specification does not handle this case. |
This feature is moving to implementation; tracking issue is dart-lang/sdk#57036 |
Out of curiosity, would abstract class Interface<T extends Interface<T>> {
void method(num value in T) {}
}
class Concrete extends Interface<Concrete> {
static final pi = 3.14;
}
class Another extends Interface<Another> {
static final theAnswer = 42;
} Then used as: final concrete = Concrete();
concrete.method(.pi);
final another = Another();
another.method(.theAnswer); |
I assume this is concerned with the proposal about 'parameter default scopes', #3834? Of course, that particular proposal caused several community members to get really, really upset, so it wasn't part of the proposal which is now being implemented. However, I still think something along the lines of #3834 would be a useful generalization of the proposal which is being implemented. This would simply amount to adding support for When there is no The parameter default scope proposal requires This means that there is no direct support for your example. However, if you're only going to use the abstract class Interface<T extends Interface<T>> {
void method(num value) {}
}
class Concrete extends Interface<Concrete> {
void method(num value in Concrete);
static final pi = 3.14;
}
class Another extends Interface<Another> {
void method(num value in Another);
static final theAnswer = 42;
}
void main() {
final concrete = Concrete();
concrete.method(.pi);
final another = Another();
another.method(.theAnswer);
} Note that the additional declarations of We could consider making abstract class Interface<T extends Interface<T>> {
void method(num value in T) {}
}
class Concrete extends Interface<Concrete> {
// Implicitly induced: `void method(num value in Concrete);`
static final pi = 3.14;
}
class Another extends Interface<Another> {
// Implicitly induced: `void method(num value in Another);`
static final theAnswer = 42;
}
void main() {
final concrete = Concrete();
concrete.method(.pi);
final another = Another();
another.method(.theAnswer);
// Forget all about `Concrete` by using a super-bounded type.
Interface<Object?> interface = concrete;
interface.method(-1.75); // `.pi` and `.theAnswer` are both errors.
} This could be a quite interesting generalization of the parameter default scopes feature! However, it (probably) implies that However, we could even allow void main() {
...
Interface<Concrete> interface = concrete;
interface.method(.pi);
} You could then say that We're cheating, though, because the actual type of such an object could very well be a type that has The latter would require a feature like 'more capable type objects', #4200, because that's a feature that allows us to use static members and constructors based on the actual value of a type parameter. This approach only makes sense if we have multiple static members or constructors with the same name, because the whole point is that we can have one call site which is capable of calling one or the other, and it isn't known until run time which one it is. For example: abstract class HasAnswer {
num get theAnswer;
}
abstract class Interface<T extends Interface<T>> {
void method(num value in T) => print(value);
}
class Concrete extends Interface<Concrete> static implements HasAnswer {
static final theAnswer = 3.14;
}
class Another extends Interface<Another> static Implements HasAnswer {
static final theAnswer = 42;
}
void foo<T extends Interface<T> static extends HasAnswer>(T t) {
t.method(.theAnswer); // Means `T.theAnswer`, which uses #4200.
}
void main() {
foo(Concrete()); // Prints '3.14'.
foo(Another()); // Prints '42'.
} This is yet another generalization of the parameter default scope mechanism, based on the 'more capable type objects' in #4200. In the basic dot shorthand feature, we can pass In the approach where we use 'more capable type objects' we could again obtain the All in all, I think it's fair to say that we can find some ways to generalize the dot shorthands such that they can be used with types that do not directly denote a declaration. However, it does get somewhat hairy. ;-) |
Admin comment: this is being implemented, feature specification, dart-lang/sdk#57036, experiment flag
enum-shorthands
.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:
The text was updated successfully, but these errors were encountered: