dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Allow for shorter dot syntax to access enum values #357

Open rami-a opened 5 years ago

rami-a commented 5 years ago

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:

enum CompassPoint {
  north,
  south,
  east,
  west,
}

if (myValue == CompassPoint.north) {
  // do something
}

The proposed alternative:

enum CompassPoint {
  north,
  south,
  east,
  west,
}

if (myValue == .north) {
  // do something
}
eernstg commented 3 years ago

@rrousselGit wrote:

with option 1 we don't have the issue?

Right, a leading '.' can be taken to indicate that we are looking up the following identifier token in a non-standard manner (that is: not lexical-scopes-followed-by-prepend-this.).

But we still have a lot of ambiguity: .foo (choosing the standard identifier name so as to avoid a bias about the semantics ;-) with context type T could denote, at least, the following:

I just don't see this as a natural collection of semantics for the given syntax, it looks more like an accidental hodgepodge of things that we can do if we're thinking about the mechanism as a syntactic device that means "prepend the missing class name (and settle the extras ;-)". I believe that this makes the code harder to read.

I see the leading . as a critical information for readers

I'm happy that we're both putting a lot of emphasis on readability!

If we really need a keyword, what about var?

I was assuming that we'd have option #2 (that is, we have no syntactic marker for lookups that yield a distinguished object). I was just asking for a good keyword in case there is a strong push to have a syntactic marker, and var as a marker for distinguished objects could be a useful choice.

The core idea that I've tried to explore is "lookup by type" as opposed to the standard approach which is "lookup in scopes, with a fall-back to "prepend this.", and that seems to fit the main use cases.

So if we are going to have any kind of syntactic marker then it would be great if it could send a clear signal that means "look up by type".

eernstg commented 3 years ago

@lrhn wrote:

With option #2, it's definitely easy to mistake it for a normal function call. The context should make it clear what's going on

Agreed, the whole idea that we'd replace MyEnum.someValue by anything which is shorter relies on a lot of "accidental" information in the immediate context: We had better assign someValue or .someValue to a named parameter with the name somethingWithMyEnum, or we had better have an explicitly declared variable type in the same line of code as the context type, etc.

(And there should not be a lint forcing you to remove the class name just because you can!)

Exactly! It must be reserved, with good human judgment, to the locations where the source code somehow says it all already.

Abion47 commented 3 years ago

Because every name which is made available (under option #2) in a way that makes them similar to top-level declarations is both a resource and a nuisance: We get easy access, but we also pollute the global namespace. I definitely think it's useful to be able to control the level of pollution of this kind.

With the proposed solution of option #1, typedefs, and extension methods via #723, I don't see this being an issue. Anything following the period in a context of type T would be the same as if typing T., so it would function as expected. The namespace pollution concern would've been if we were considering the inclusion of members from other arbitrary types either extending or not directly related to T, which with the aforementioned solution would be unnecessary.

  • An enum value, if T is an enum type.
  • A static declaration of a variable or getter in the class of T, if T is a class or a generic instantiation of a class, and the variable has a type which is the class of T, or some generic instantiation thereof (I'm not quite sure which relationships are allowed).
  • A static method in the class of T (same extras as above), whose return type is the class of T or some generic instantiation thereof (I don't know why we'd want to single out those static methods, but here we go).
  • Depending on the acceptance of something like Static extension methods #723: A static method of an extension whose on type has some relation to T (maybe it should be the same type, maybe T should match the on type), when that method has a suitable return type (same extras as above).
  • The second half of the name of a constructor declared by the class of T (plus the usual extras).
  • Depending on Static extension methods #723: The second half of the name of a factory constructor declared by an extension on some type with a suitable relation to T (plus the usual extras).

I don't see this as an accidental hodge-podge. I see this as the minimum list of necessary potential supported resolutions for this feature to work where expected to avoid situations where a user can use the shorthand syntax for some things but not for others, resulting in inconsistency and therefore frustration. In other words, if I was the average user, any place I would type T.value or T.constructor(...) or T.method(...) where the context T could be inferred, I would expect to be able to use .value, .constructor(...), and .method(...) (assuming option #1).

Levi-Lesches commented 3 years ago

I just don't see this as a natural collection of semantics for the given syntax, it looks more like an accidental hodgepodge of things that we can do if we're thinking about the mechanism as a syntactic device that means "prepend the missing class name (and settle the extras ;-)". I believe that this makes the code harder to read.

I believe it's more readable if .foo is simple a syntax sugar for T.foo, because users already know what T.foo means, despite there being a long list as you pointed out. And IDE's already have autocompletion for T.fo --> T.foo. As I understand it, this issue is meant to shorten some cumbersome named parameters, like the following.

child: Column(
  mainAxisAlignment: MainAxisAlignment.start, 
  crossAxisAlignment: CrossAxisAlignment.start,
  child: Container(
    alignment: Alignment.bottomLeft, 
    color: Colors.red, 
    padding: EdgeInsets.all(5),
    margin: EdgeInsets.all(10),
    child: Text(
      "Hello, World!",
      textAlign: TextAlign.center,
    ),
  )
),

// shortens to 

child: Column(
  mainAxisAlignment: .start, 
  crossAxisAlignment: .start,
  child: Container(
    alignment: .bottomLeft, 
    color: .red,  // with static extensions 
    padding: .all(5),
    margin: .all(10),
    child: Text(
      "Hello, World!",
      textAlign: .center,
    ),
  )
),

I believe that by being a simple shorthand in cases where T is obvious and nowhere else, this feature help readability more than hurting it.

rrousselGit commented 3 years ago

I agree with @Levi-Lesches that this feature can help readability.

I see it as a way to reduce visual noise by removing redundant/obvious information.
The scope of this feature is clearly for cases where functions/parameters names make it explicit. It's quite similar to CSS:

.container {
  background-color: red;
  border-radius: 20px;
  border: solid 2px black;
}

That border: solid 2px black is not any less readable than border: Border(BorderType.solid, 2px, Colors.black), since the name border is enough of a context clue to know what we're talking about.

So in that sense, I don't think the Flutter equivalent is problematic:

BoxDecoration(
  color: .red,
  border: .all(color: .black, width: 2, style: .solid),
  borderRadius: .all(.circular(20)),
)

I do agree that this feature can backfire though, so I get the pushback. People could over-use it.
But then again, people could name all their variables i or make all their variables dynamic.

lrhn commented 3 years ago

The way this has so far been specified, the only entire expressions that this applies to have the form .foo or .foo(args) (potentially with type arguments too). It does not apply if you write something afterwards. It does not apply if that expression would not have the correct type.

But we still have a lot of ambiguity: .foo (choosing the standard identifier name so as to avoid a bias about the semantics ;-) with context type T could denote, at least, the following:

  • An enum value, if T is an enum type.

Yes.

  • A static declaration of a variable or getter in the class of T, if T is a class or a generic instantiation of a class, and the variable has a type which is the class of T, or some generic instantiation thereof (I'm not quite sure which relationships are allowed).

(or T denotes a mixin or instantiated generic mixin type.)

Yes, if the type of the static getter is assignable to the context type. If that type is generic, that depends on the value. You can have:

class C<T> {
  static const int = const C<int>();
  static const String = const C<String>();
  const C();
}
C<num> x = .int; // valid.
C<String> y = .int; // Fails. `int` found on `C`, but does not have the correct type.
  • A static method in the class of T (same extras as above), whose return type is the class of T or some generic instantiation thereof (I don't know why we'd want to single out those static methods, but here we go).

Nope. A static method tear-off (which is what you get from .foo) will have a function type, and since no function type has a foo static member (or any static member), that's always an error.

If you wrote .foo(args), then you could call the static function foo on T if it returns something of type T.

  • Depending on the acceptance of something like #723: A static method of an extension whose on type has some relation to T (maybe it should be the same type, maybe T should match the on type), when that method has a suitable return type (same extras as above).

The requirements of #723 for denoting a class would be the same we use for when a type alias denotes a class/mixin for static member access. If it's a class/mixin type, possibly instantiated, then the static members apply to that class/mixin declaration. (And also the same we use for the context type to figure out which class/mixin to look in, basically remove the type arguments, what remains must denote a class/mixin declaration).

So:

extension FunnyList<T> on List<T> {
  factory FunnyList.random(int count, List<T> values) => [for (var i = 0; i < count; i++) values[random.nextInt(values.length)]];

  static List<int> randomInts(int count) => [for (var i = 0; i < count; i++) random.nextInt(count)];
}

...
   List<String> l = .random(5, ["a", "b"]); // Works, implicit constructor call.
   List<Object> l2 = .randomInts(5); // Works, implicit static function call. Return type matches.
  • The second half of the name of a constructor declared by the class of T (plus the usual extras).

Not unless you call it. In that case, yes.

  • Depending on #723: The second half of the name of a factory constructor declared by an extension on some type with a suitable relation to T (plus the usual extras).

Yep, if you call it.

But there is never any doubt.

If you write .foo().bar(), you get an error. There is no context type on .foo().

Now, you can probably write .foo()..bar(), which ... can likely even be useful. Foo foo = .allocate()..register(registry);. I'm OK with that. It still starts with a . so you can look ahead and see if it's one identifier, or if the identifier is followed by arguments. That's the only two options.

And it always means the same as C.staticGetter, C.staticMethod(args) or C<T>.constructor if the context type is C<T>.

TimWhiting commented 3 years ago

There is a balance in language design. On the one hand you want to enable concise expressive code, on the other hand you want to reduce the chance of people shooting themselves in the foot. I get that there needs to be a balance. It's fairly clear though that there would be no more runtime errors here than there was previously. Statically you would know if there was an ambiguity error and require the user to resolve it by typing out the receiver. The only argument I see against this is readability in non-IDEs for positional parameters where the reviewer isn't aware of the context type and it is not obvious from the enum reference. The benefits outweigh the chance for abuse in my opinion.

For example. This is a lot harder to read

BoxDecoration(
  color: Colors.red,
  border: Border.all(color: Colors.black, width: 2, style: BorderStyle.solid),
  borderRadius: BorderRadius.all(Radius.circular(20)),
)

than

BoxDecoration(
  color: .red,
  border: .all(color: .black, width: 2, style: .solid),
  borderRadius: .all(.circular(20)),
)

In fact in the first one, I have a hard time reading the important bits of information which makes code reviews and refactorings harder. The second one improves the maintainability of the code.

As a side note I always have to dig into the borderRadius documentation to remember what I need to put in the BoxDecoration parameters because the types are:

BoxDecoration(Color color, BoxBorder border, BorderRadiusGeometry borderRadius)

but both BoxBorder and BorderRadiusGeometry are abstract types and cannot be instantiated, so you have to dig into their documentation which incidentally is not available from the hover information in VSCode when you are hovering over BoxDecoration or it's parameters, so you have to drill down into BoxDecoration first, then find the borderRadius field (not the parameter in the constructor since it uses this.border) and then drill into the BorderRadiusGeometry type there, and then you finally find that the BorderRadius subclass is the one that you want. For newcomers this is counterintuitive, even though the name of the parameter is the same name as the class that you want. Personally I look at the type information first to understand something I don't know, and when the IDE cannot autocomplete from BorderRadiusGeometry a static method or anything I get confused.

In dart I almost never have to go to stack overflow / google because of being able to click through documentation and use the IDE intellisense to find properties and methods I need. Contrast that to python where I always have to stackoverflow things because they have the *args, **kwargs syntax that make it impossible to know what you can pass in. But I have several times had to search stackoverflow for the Border stuff in Flutter to figure out how to do something.

I get that by exposing all subclass constructors/factories such as .all you might then have ambiguity between the two subclasses BorderRadiusDirectional.all and BorderRadius.all, but at least they would show up in the autocomplete when typing .all and I would be able to choose the one that I want. Alternatively box decoration could have a .directional factory constructor that only takes the directional subclass, or the BorderRadius superclass could add a factory to create directional or not based off of a bool. The point is that this is now an API design choice when you expose abstract classes with two subclasses with named constructors that are the same, but that even when there are conflicts the autocomplete can give you the option and you can select the one you want. Or you can disallow showing subclass constructors and require people to add static extensions on the superclass if they want to expose a subclass constructor.

rrousselGit commented 3 years ago

If you write .foo().bar(), you get an error. There is no context type on .foo().

Interesting. I would've expected .foo().bar() to work

I see T value = . as syntax sugar for T value = T., no matter what is after the dot.
I believe there should never be a case where the implied type before . is anything but T

So in that sense, we could even allow operations:

Color color = .red + .green

And the following:

int value = .parse('42').abs() / 2

would fail to compile with "double is not assignable to int" instead of a syntax error.

Abion47 commented 3 years ago

Nope. A static method tear-off (which is what you get from .foo) will have a function type, and since no function type has a foo static member (or any static member), that's always an error.

@lrhn Not necessarily. Currently, you can write extension methods on function types (which is a bizarre feature in and of itself):

extension on void Function(int) {
  int getNum() => 1;
}

void foo(int i) {
  print(i);
}

void main() {  
  print(foo.getNum());
}

So with #723, it stands to reason that you would be able to create static extension members on function types as well, leading to situations where this would no longer be an error (which has some interesting implications for possible utility in various places in Flutter that currently utilize callback properties):

extension on void Function(int) {
  static void Function(int) get foo => (i) => print(i);
}

void bar(void Function(int) callback) {
  callback(5);
}

void main() {
  bar(.foo);
}
lrhn commented 3 years ago

With #723, it is possible for a static method tear-off on a function type to have the correct type. The example here isn't that, it's a getter. An example would be:

extension on int Function(int) {
  static int id(int x) => x;
}

int Function(int) intId = .id; // Tear-off of static method where context type matches expression type.

We should probably allow that, it's perfectly reasonable code, and the .id function is likely intended as a "known value of the type", just as similar constants like double.infinity. (I tend to think of tear-offs as calling an implicit getter introduced by the method declaration, and having the same name).

I can see why doing .something.selectors + operators can be reduced to looking up the leading .name on the context type of the entire expression. It's a little weird because the context type of .something + 1 is not the context type of .something (it has no context type because its value is not going into any context, it's just used as receiver for the + operator). So, we'd have to define a "leading .name expression" specially in the grammar, and define how far it goes (and define it at each precedence level so you can do .a + .b * .c and give it a meaning. Or pass along an "implicit context type" to the left-most operand, even when it gets no actual context type, and use that as the target for .name static lookups. That's probably easier than doing changing the grammar.

I'm sure we can define it somehow, but I'm not sure it's good for readability or complexity of the language.

(So I prefer a more restricted approach only targeted at enum-like values, only allowing .foo to invoke static getters (and possibly static method/constructor tear-offs) and .foo(args) to invoke static methods/constructors on the context type. And I do prefer it to omitting the . because it makes it obvious that something special is going on.)

Abion47 commented 3 years ago

I tend to agree that we wouldn't want to allow chained resolvers like .something.selectors just for simplicity's sake. The only time it would work with the context is if (T).foo and (T).foo.bar both resolved to something of type T, which IMO is a fairly uncommon case. At first I thought it would help for Theme stuff in Flutter, like whenever you need to call Theme.of(context).copyWith(...) (and other such X.of(context) things). However, those contexts require ThemeData rather than Theme, and while this might be rectified using the same solution as for Color/Colors, I imagine that one would require a much more involved (and potentially breaking) refactor.

Other operations like arithmetic, however, should definitely be supported. It would be useful for things like Offset, Rect, and other geometry and math-related functions, especially matrices. For example, here's some vector stuff:

class Vector2 {
  final double x;
  final double y;

  static const Vector2 right = Vector2(1, 0);
  static const Vector2 up = Vector2(0, 1);

  const Vector2(this.x, this.y);

  Vector2 operator +(Vector2 other) => Vector2(x + other.x, y + other.y);
  Vector2 operator -() => Vector2(-x, -y);
}

...

Vector2 upRight = .up + .right;
Vector2 downRight = -.up + .right;

The way I see it working, the inferred type of the left-hand side of the assignment would become the implicit type T in the entire right-hand side. So for anywhere in the body of that expression, the type T can be omitted when used for static references (provided there isn't a change in context like in the parameter list of a function call within the expression). And a similar implicitness would occur for other situations like switch cases or function parameter lists.

On a side note, is the -.up example weird here, though? It certainly looks weird. Maybe there should be a recommendation (or lint) that unary prefix operators on this short form include a parentheses (i.e. -(.up))?

nidegen commented 2 years ago

Please just make it happen;)

God I hate crossAxisAlignment: CrossAxisAlignment.start when it could just be crossAxisAlignment: .start, ..

aodyshakir commented 2 years ago

👍

Linkadi98 commented 2 years ago

I have a question, this feature can have any chances in future release? It's 2022 guys :(

clocksmith commented 1 year ago

True story related to issue:

my 11 year old nephew, who only knows roblox and minecraft programming, was peaking over my shoulder as I was coding some Flutter. Something like

enum NavigationAnimation { left, center, right }

NavigationBar navigationAnimation = NavigationAnimation.left;

He starts reading "navigation animation equals navigation animation period left...wait why are you typing navigation animation again?"

Edit: This is not the primary use case for this feature request, just thought it was interesting to hear from the next generation of software engineers. I think the core issue is more about the readability and compactness trade offs than having to type it, since basically all code editors will autocomplete this for you.

Hixie commented 1 year ago

You could write var navigationAnimation = NavigationAnimation.left if you want to avoid typing it again. Or you could use an IDE that has better autocomplete. The real answer to "why are you typing it again", though, is "so that you know which left I'm referring to". There's a lot of interesting discussion buried in the "load more" links above that I think give pretty compelling reasons for not adding this feature, IMHO.

Abion47 commented 1 year ago

@Hixie The biggest reason not to add this feature IMO is that the Flutter library wasn't really designed with it in mind which means many of the most logical places to use it won't be able to (for example: with Container(color: .red), where color is expecting a dart:ui » Color but .red is coming from the flutter » material » Colors class), and any of the proposed ways to make it work would come with a slew of problems on their own that make them impractical.

On the other hand, Dart 3 brings a compelling argument that the language needs this feature now more than ever:

enum Animal {
  dog,
  chicken,
  man,
  oldMan,
}

...

Animal animal = ...;
int numOfFeet = switch(animal) {
  Animal.dog => 4,
  Animal.chicken => 2,
  Animal.man => 2,
  Animal.oldMan => 3,
};

(Yes, it's contrived.)

With pattern matching having entered the equation, enums and enum-like classes are likely going to be used more than ever before which will greatly exacerbate the pain point of having to redundantly type the type name so many times. That means that this solution (or something similar) will probably need to be addressed at some point sooner or later.

Hixie commented 1 year ago

The pain of typing these things is really a non-issue, IMHO. That's an IDE problem. Honestly with something like a switch you would expect your IDE to offer to autocomplete the entire switch from the moment you typed switch (animal). Even without Dart-aware autocomplete you it would be trivial to add Animal. on every line with any modern IDE. The language should be optimized for reading, not writing.

nidegen commented 1 year ago

As a long time Swift user, I just never saw any drawback of having the value name only. Most often the variable or parameter name does reflect the type anyways. It just feels so much more efficient and concise. Would really love to see it!

I actually do believe that it would improve reading!

rrousselGit commented 1 year ago

The "Flutter isn't ready for it" issue seems like a non issue to me

First, the community don't mind breaking changes if they improve the code. We also have dart fix if need be.

And I'm sure things like Colors could be changed from abstract classes to static extensions instead. This would allow both Colors.myColor and Color.myColor to resolve at once. It probably wouldn't be breaking then, as Colors is never instantiated/implemented

lrhn commented 1 year ago

(If you can have both Color.red as a static (extension or not) and instance member on Color - but today you can't.)

That said, that a feature doesn't solve all problems isn't necessarily an argument against it solving some problems. Like enums.

rrousselGit commented 1 year ago

(If you can have both Color.red as a static (extension or not) and instance member on Color - but today you can't.)

I don't know what this is referring to.
Why would we need red as an instance member on Color? All access to the red color is done through a static property.

lrhn commented 1 year ago

Referring to Color.red

rrousselGit commented 1 year ago

My bad, I forgot about the rgb properties

Hixie commented 1 year ago

Color and Colors aren't enums so this issue doesn't apply to them anyway.

rrousselGit commented 1 year ago

Color and Colors aren't enums so this issue doesn't apply to them anyway

@Hixie There are a few arguments in this thread that this shouldn't be enum-specific, but rather about static members.

To be able to do things like borderRadius: .circular(10)

clocksmith commented 1 year ago

You could write var navigationAnimation = NavigationAnimation.left if you want to avoid typing it again. Or you could use an IDE that has better autocomplete. The real answer to "why are you typing it again", though, is "so that you know which left I'm referring to". There's a lot of interesting discussion buried in the "load more" links above that I think give pretty compelling reasons for not adding this feature, IMHO.

Interesting, edited my original post to focus on the main issue, and will take a look at the buried threads for counter arguments. Perhaps we can pin/link a table to the top this issue that list all of the tradeoffs.

@rami-a

jacob314 commented 1 year ago

I find the shorter dot syntax much easier to write but slightly harder to read. Relative to 2019 I think it now makes sense to prioritize reading more strongly over writing as LLMs continue to make writing code easier but help less with reading and understanding it.

To reduce the burden manually writing code like this, we can improve the autocomplete experience for enum and static member access without changing the language. For example: we could make the autocomplete experience after typing borderRadius: . include all the results from borderRadius: BorderRadius. and we could offer a quick fix from borderRadius: .circular(10) to borderRadius: BorderRadius.circular(10) applied automatically on save in VSCode. This could give you 90% of the code editing benefits of the terse syntax without the code readability costs.

csells commented 1 year ago

I find the shorter dot syntax much easier to write but slightly harder to read.

I disagree -- I think that the dot syntax is both easier to write and easier to read, the latter due to less text, especially redundant text. I find JSON easier to read than XML for that same reason (with YAML even easier to read than JSON, continuing the analogy).

rrousselGit commented 1 year ago

@jacob314 This has been argued many times in this thread, but overall a lot of people think that this would improve readability.

It's all about context clues, and making code flow more like a natural language. In the end, we say "My name is Remi", not "My name is Name.Remi"

Those namespaces aren't useful for readers. They are for the compiler. In many other languages, we wouldn't type those.

For example, in JS/TS, it's very common for APIs to expose a union of strings. Such that we can write:

If (status == "loading")
else if (status == "error")
else...

Where status is "loading" | "error" | "data"

Abion47 commented 1 year ago

@rrousselGit On the other hand, namespaces are useful for writers in that they increase API discoverability. If you're typing code and you reach the point:

if (state == // Now what?

What should you type next? It's virtually impossible to know without consulting a source file or documentation, and outside of specific scenarios (or AI-assisted completion tools), there's a limited amount of help the IDE can offer. A namespace, however, can get Intellisense to kick in:

if (state == WidgetState. // Suggestions: loading, ready, error, idle, fetchingData, ...

So while it's good to prioritize code that's readable to humans over being readable to computers, it's also important to not do so with unnecessary impact to writability and discoverability.

(And to keep this relevant to the thread, the benefit of the dot syntax is that it carries an implicit namespace as dictated by the context, and is an indicator to the IDE to fetch said context for Intellisense. No-dot syntax would require the IDE to have to consider that the context could prompt for enum-like autocompletion at virtually any time, which could greatly increase the complexity of the extension design.)

cedvdb commented 1 year ago

@Abion47 this seems easier to discover

if (state == // Now what?

You could press . , assuming you'd know in advance that state is an enum and that compiler knows it's WidgetState enum.

A namespace, however, can get Intellisense to kick in if (state == WidgetState.

You still have to find the WidgetState to type it, don't you ?

Abion47 commented 1 year ago

@cedvdb That's my point, that omitting the . makes it "easier to read" but at the cost of discoverability.

In other words, this:

if (state == idle)

is (subjectively) slightly more readable than this:

if (state == .idle)

But writing the former is way harder than writing the latter because you wouldn't be able to type . to trigger Intellisense. You would have to just know the value you want, and if you don't know, you'd have to look it up.

The solution might be to have Intellisense trigger after pressing space bar after ==, but that means the extension would need to watch for all triggers in which that autocomplete could happen. Sure, it could be done, but not without a significant amount of non-trivial work. With dot notation, it would trigger off of ., same as it does now, with the only difference being that the namespace is inferred by the type context.

My other issue is that the former doesn't just make it harder when writing to know what value to type, it also makes it harder when reading to know where the value comes from. The no-dot syntax just has a simple identifier, so how do you know if that identifier is using the syntactic sugar for enum access and not referencing a local variable, a class field/getter, or a global constant?

// is `idle` referencing `WidgetState.idle` or is it referencing the `idle` getter, variable, field, or constant 
// somewhere in the current context? no way to know for sure by just reading this code
if (state == idle) 

The latter, however, makes it clear.

// `.idle` is unambiguously an enum or static member that matches the context type of `state`
if (state == .idle)
rrousselGit commented 1 year ago

@cedvdb That's my point, that omitting the . makes it "easier to read" but at the cost of discoverability.

First, I wasn't really arguing for not using .. It's an important context clue for readers too in Dart's world imo. I was only sharing an example from a different language.

But if you're referring to my typescript example, there's no discoverability issue. The autocompletion correctly suggests "loading" vs "error" vs "data".

That's because any value other than this is considered as unreachable code, thanks to union types.

cedvdb commented 1 year ago

What does it mean, exactly, for .something to be less readable ? Can this be fixed with a lint rule ?

There might be some context lacking here

if ( something == .loading) 

But it doesn't seem like it here:

Column(
  mainAxisSize: .max,
  mainAxisAlignment: .end,
  crossAxisAlignment: .start,
rrousselGit commented 1 year ago

There might be some context lacking here

if ( something == .loading) 

I would assume that for ==, that's fine. Because in practice, you would have better variable names.
A more realistic if would be if (asyncSnapshot.connectionState == .waiting), which is way more explicit

Honestly, I'm not sure we need a lint rule.
I would assume that if when using .value we're missing context clues, the parameter probably should've been a named parameter instead.

Abion47 commented 1 year ago

@rrousselGit

First, I wasn't really arguing for not using .. It's an important context clue for readers too in Dart's world imo.

I wasn't making any particular argument in that initial reply either. I was simply pointing out that namespaces have a visual purpose even if they can add clutter. Including namespaces in identifiers isn't always just for the compiler's benefit, it can be for the readers' as well.

That's because any value other than this is considered as unreachable code, thanks to union types.

Typescript solves the discoverability issue by tying the IDE into the type union system, yes. Except Dart has no such system. I get the point you were making before, I'm just not sure how it's relevant beyond being an interesting anecdote. (Unless you're implying that this feature can't be meaningfully implemented until Dart supports type unions?)

@cedvdb

What does it mean, exactly, for .something to be less readable ?

For a similar reason that some people find semicolons and parentheses/brackets less readable. Punctuation is an additional visual indicator, and where some people find it clarifying, others find it distracting. The issue is balancing how many people find it distracting, how distracting they find it, and weighing it against other practical alternatives (if any) and the functional benefits of the feature. Sometimes a feature gets scrapped because people hate it even though it has objective value, and sometimes a feature gets implemented despite the hate because it has too much objective value. Ultimately, that's for the Dart team to decide.

ReinBentdal commented 1 year ago

There might be some context lacking here

if ( something == .loading) 

The might be a case for the dot syntax resulting in overall better variable names, because its more obvious when a variable name is bad as in this example

DanTup commented 1 year ago

I'm late to this party, but I often hear complaints about ranking of enum (and enum-like) values in code completion. VS Code re-ranks code completion client-side as soon as there is a prefix it can use for filtering. This means the analysis server may rank like this:

Well ranked code completion

But as soon as you start typing "max" you end up with this:

Badly ranked code completion

VS Code has no type information and assumes anything starting with "max" is definitely a better match than something ending with ".max". Requests for type-context-sensitive completion were rejected.

It's difficult to filter the completion list by default because the user might really want to use foo.bar().baz.myEnumValue and having anything missing from completion looks like a bug. But maybe we could use typing . as a signal to provide a filtered list?

Here's an example showing the normal behaviour, and what it could look like if "." was used to influence completion. Selecting an item will insert the full value:

https://github.com/dart-lang/language/assets/1078012/efcf1ab1-f693-4717-bdc2-c180bc636347

This certainly doesn't deliver all the potential benefits discussed above, but if some of the 👍 s are motivated by the frustrating completion ranking, maybe there's value in it? It doesn't need to worry as much about finding the exact item the user wanted, because we can still show a few and let them pick the correct one (something much easier when there are 5 items compared to when there are 1,000).

cedvdb commented 1 year ago

in practice, you would have better variable names.

Not everyone will. There might be an argument that the variable name is required to be some case version of the enum name for it to still be readable. (connectionState == .waiting when enum ConnectionState { waiting, done })

What about ?

Card(
    clip: .antiAlias // ClipBehavior.antiAlias

When reviewing code on github, without the possibility to hover a .antiAlias to see its full name, you'd have yet another barrier to find where .antiAlias is coming from. Which dart imports already make hard. At that point you'd have to go check Card source code to find the type and then do a github search to find were the type is declared since you can't rely on imports. This adds one more step.

Don't get me wrong though, I want this feature but I think the lint rule would be very useful here.

jacob314 commented 1 year ago

What @DanTup demoed means you get the benefits of the .waiting syntax at code editing time without the downside of obfuscating what the code or being restricted to unambiguous cases. In practice the shorter dot syntax will struggle with many common Flutter use cases that look similar to enums but are too different for the dot syntax for work. For example, with @DanTup's option, the IDE can do exactly what you would expect for alignment parameters, showing the statics defined by both AlignmentDirectional and Alignment (Alignment directional have conflicting static field names so shorter dot syntax can't work). Similarly, colors and icons can be handled well with this option but are not feasible with the shorter dot syntax given how the classes are defined in Flutter.

mraleph commented 1 year ago

In practice the shorter dot syntax will struggle with many common Flutter use cases that look similar to enums but are too different for the dot syntax for work.

Why not both? Completion improvements don't replace language improvements and vice versa.

Just because .name syntax does not work for some cases in Flutter does not mean we should not do it. If anything a) Flutter is not Dart and Dart is not just language for Flutter and b) Flutter is not set in stone, it can and (arguably should) change in response to external feedback and new Dart features.

If anything Swift's experience shows that the syntax works very well, especially when ADTs are considered.

We need to stop looking at this as just a feature to "save a bit of typing when addressing enums" and see it as part of the bigger thing: making concise domain modeling possible.

Consider for example the following code:

sealed class Status {
}

class StatusLoading extends Status {
  // ...
}

class StatusLoaded extends Status {
}

Status currentStatus(SomeNetworkEvent e) {
  if (...) {
    return StatusLoaded(e.data);
  } else {
    return StatusLoading(e.progress);
  }
}

Widget buildSomething(Status status) {
  return switch (status) {
    StatusLoaded(:final data) => /* ... */,
    StatusLoading(:final progress) => /* ... */,
  };
}

what if this was possible instead:

sealed class Status {
  factory Status.loading({required double progress}) = StatusLoading;
  factory Status.loaded({required Data data}) = StatusLoaded;
}

class StatusLoading extends Status {
  // ...
}

class StatusLoaded extends Status {
}

Status currentStatus(SomeNetworkEvent e) {
  if (...) {
    return .loaded(e.data);
  } else {
    return .loading(e.progress);
  }
}

Widget buildSomething(Status status) {
  return switch (status) {
    .loaded(:final data) => /* ... */,
    .loading(:final progress) => /* ... */,
  };
}

with meta-programming and/or concise way to declare sealed families this could become:

sealed class Status {
  class Loading({required double progress});
  class Loaded({required Data data});
}

Status currentStatus(SomeNetworkEvent e) {
  if (...) {
    return .Loading(e.data);
  } else {
    return .Loaded(e.progress);
  }
}

Widget buildSomething(Status status) {
  return switch (status) {
    .Loaded(:final data) => /* ... */,
    .Loading(:final progress) => /* ... */,
  };
}

Would you argue this is less readable? Anything that removes unnecessary repetition arguably makes things more readable simply because developer has to read less.

rrousselGit commented 1 year ago

What @DanTup demoed means you get the benefits of the .waiting syntax at code editing time without the downside of obfuscating what the code or being restricted to unambiguous cases.

@jacob314 I don't see it this way.

As discussed multiple times, the feature isn't inherently about writability. It's also a readability issue, by removing redundancy. Autocompletion does nothing toward that.

But which unsupported Flutter case are you talking about exactly?

We need to stop looking at this as just a feature to "save a bit of typing when addressing enums" and see it as part of the bigger thing: making concise domain modeling possible.

Big +1.

The title of this issue seems to limit the conversation quite a bit. I wonder if someone could maybe rename it?

I also think this issue is closely tied to the static extension issue (which has even more upvotes than this one) https://github.com/dart-lang/language/issues/723.
IMO https://github.com/dart-lang/language/issues/723 is the key to solving the extendability issue and dealing with numerous Flutter scenarios, like Colors adding new colors to the color: .cyan.

And there's also the issue to allow static members to have the same name as class members. (can't find the link). That would solve the Color.red edge-case

jacob314 commented 1 year ago

As discussed multiple times, the feature isn't inherently about writability. It's also a readability issue, by removing redundancy. Autocompletion does nothing toward that. I agree there is an element of readability in this feature. All things being equal I find terse syntax more readable. However, I find cases where syntax is made more terse in ways that leave me guessing at what the code is actually doing slower to understand.

But which unsupported Flutter case are you talking about exactly?

For example, consider the following Flutter code:

Container(
  constraints: BoxConstraints.expand(
    height: Theme.of(context).textTheme.headlineMedium!.fontSize! * 1.1 + 200.0,
  ),
  padding: const EdgeInsets.all(8.0),
  color: Colors.blue[600],
  alignment: Alignment.center,
  transform: Matrix4.rotationZ(0.1),
  child: Text('Hello World',
    style: Theme.of(context)
        .textTheme
        .headlineMedium!
        .copyWith(color: Colors.white)),
)

Here it would seem like the shorter dot syntax should apply to at least the alignment parameter but even with https://github.com/dart-lang/language/issues/723 (which I think is great) that is not feasible because Alignment.center and DirectionalAlignment.center would be equally important to include as static extension on AlignmentGeometry. Even if you could, it would be harmful for readability to hide from readers whether Alignment or DirectionalAlignment was used.

rrousselGit commented 1 year ago

I am sure the framework could adapt to the new feature on those specific cases.

For example, one of the many alternatives is to offer:

alignment: .center,
// vs
alignment: .directionalCenter,

I'm sure an api could be found which would match the requirements people have

And the beauty of static extensions is that if folks are unsatisfied with the current one or none is provided, they can make their own.

erf commented 1 year ago

It's perhaps silly, but i enjoy my code being aesthetically pleasing and sometimes e.g. when using this new switch expression with an Enum the line is too long and must be broken, but only on one line that is, not the others, symmetry is broken, and i can't use block body on the new switch expressions to fit them all in the same way, as i do with some functions, so i try to shorten variables names, but they won't fit still .. so please allow for shorter dot syntax, so my lines are not broken 🙏🥹

bernaferrari commented 1 year ago

I am 100% pro this change (my second-third favorite feature after Unions and Data Class). This is a small shortcoming, but it is fine: https://twitter.com/i/web/status/1686456074034143232

Apple changed Swift's .foregroundColor(.primary) (Color.primary) to .foregroundStyle(), but .primary is style.primary, not color.primary.

lutes1 commented 1 year ago

dart feels like a century old language to a Swift developer precisely because it lacks this feature, please make it happen

mym0404 commented 12 months ago

Yeah, we would make Flutter beginner happy if docs shows like that for their first Widget example.

Column(
  crossAxisAlignment: .stretch,
  children: ...,
)
HaloWang commented 11 months ago

I believe this issue is somewhat off-topic. We should avoid discussing code style, but I hope that the Dart language could provide developers with more options.

If we only focus on the enum keyword, it might be easier to implement. We can use analysis_options.yaml to lint the code, or the Dart VS Code extension can offer suggestions when we type a period . while passing parameters to a function.