dart-lang / language

Design of the Dart language
Other
2.61k stars 200 forks source link

Dot syntax for static access #3616

Open nate-thegrate opened 4 months ago

nate-thegrate commented 4 months ago

Proposal

If a type T can be inferred, then interpret .foo as T.foo.

This is similar to #357, but with a broader use case: it applies to enums, static members, and also factories & constructors.


Example

// enums
Brightness brightness = .dark;
final brightness = .dark; // error: no inferred type

// static members
bool isArrowKey(LogicalKeyboardKey key) => switch (key) {
   .arrowDown || .arrowLeft || .arrowRight || .arrowUp => true, // fits on a single line!
  _ => false,
};
if (key == .arrowUp) {} // error, since == operator doesn't infer type

// factories & constructors
HSLColor favoriteColor = .fromColor(Color(0xFF00FFFF)); // factory
HSLColor favoriteHue = .fromAHSL(1, 180, 1, 0.5); // named constructor

// you don't write a dot for the unnamed constructor, so it doesn't change
const Duration twoSeconds = Duration(seconds: 2);


Here's everything put together:

@override
Widget build(BuildContext context) {
  return Container(
    width: .infinity,
    margin: .zero,
    padding: .symmetric(vertical: 8.0),
    decoration: BoxDecoration(
      color: .fromARGB(255, 0, 128, 255),
      shape: .circle,
    ),
    child: widget.child,
  );
}



Later On

Static Analysis

If/when this is implemented, it should probably come with some additional linter rules:

from keyword

I really liked the keyword idea from #1955 and would love to see it implemented at some point down the line.

In that issue, from was added after the parameter name. I think that attaching it to the type would be best but would be happy either way.

abstract final class MyColors {
  static const chartreuse = Color(0xFF80FF00);
  static const spring     = Color(0xFF00FF80);
  static const vermilion  = Color(0xFFFF4000);
  static const indigo     = Color(0xFF4000FF);
}

typedef MyColor = Color from MyColors;

class AnimatedIcon extends StatelessWidget {
  const AnimatedIcon({
    required this.icon,
    required this.duration,
    required this.curve,
    required this.color,
  });
  final IconData from Icons icon;
  final Duration from Durations duration;
  final Curve from Curves curve;
  final MyColor color;
  ...
}

final icon = AnimatedIcon(
  icon: .home,
  duration: .medium1,
  curve: .easeOut,
  color: .spring,
);
nate-thegrate commented 4 months ago

It's worth noting that the example from #357 wouldn't work under this proposal.

enum CompassPoint { north, south, east, west }

if (myValue == .north) {} // this should throw an error,
                          // since the other side of the == operator could have any object

But the additional keyword would make it work:

class Foo {
  bool operator ==(Object from Foo other) {
    ...
  }
}

And without a from keyword, a lack of support for == would at least be in accordance with the Flutter style guide:

Avoid using == with enum values

AlexanderFarkas commented 4 months ago

In my opinion it brings little value, but greatly decreases code readability. You never know, where the dotted member comes from.

Consider:

SomeWidget(
  color: .blue // is that MyAppColor? CupertinoColors? Just Colors? - you never know until you hover over it.
)
mateusfccp commented 4 months ago

In my opinion it brings little value, but greatly decreases code readability. You never know, where the dotted member comes from.

Consider:

SomeWidget(
  color: .blue // is that MyAppColor? CupertinoColors? Just Colors? - you never know until you hover over it.
)

The proposal of this issue would not include this case. It would work for enums, static members, factories & constructors of a given type T that is inferred by the context.

So, if SomeWidget receives a parameter Color color, MyAppColor, CupertinoColors and Colors would never be considered for .blue. Only Color would. And as Color does not have a .blue static member, an error would be emitted.

AlexanderFarkas commented 4 months ago

@mateusfccp Thank you for the explanation, I had the wrong implementations in my mind.

Yet, the issue is still there.

enum CupertinoColor {
  blue("0xXXA");

  const CupertinoColor(this.color);
  final String color;
}
enum MaterialColor {
  blue("0xXXB");

  const MaterialColor(this.color);
  final String color;
}
enum MyColor {
  blue("0xXXC");

  const MyColor(this.color);
  final String color;
}

// ...
SomeWidget(
  color: .blue // You don't know where this `.blue` comes from until you see SomeWidget declaration.
);

Though most of the time you don't have ambiguous declarations, I'm just bringing it up so it could be considered.

mateusfccp commented 4 months ago

Sure, you would have to know whether SomeWidget accepts MyColor, MaterialColor or CupertinoColor.

Personally, I don't see this as a huge disadvantage, and you can always use the full reference. We would probably also have linters to enforce the full or dot syntax, depending on the preference of the team.

lrhn commented 2 months ago

I have added somewhat larger-in-scope design proposal.

It has some ideas for how to allow more constants than just on the type itself, but it still cannot reach Flutter's Colors class. I don't think any reasonable design can.

tatumizer commented 1 month ago

This^ design proposal is similar to swift's implicit member expressions, except that for "other operators", swift allows only a chain of postfix operators, which always starts with the .id member expression (see examples in the linked doc).

lrhn commented 1 month ago

The postfix operator allows operations like .values.first or .values.byName[json["name"] as String] for enums. It's not unreasonable, and really means that .selectors means contextType.selectors.

I'd probably not allow looking in subclasses or related classes if we also allow postfix expressions. That would open for too many combinations, and increase the risk of conflicts.

tatumizer commented 1 month ago

I think this kind of treatment simplifies design and simultaneously expands the scope of the feature.

Suppose Foo is a context type. When the user, say, types Foo foo = ., IDE suggests ALL static methods defined in class Foo (not in subclasses certainly), and then it's the user's responsibility to ensure that the whole chain of postfix expressions that starts with selected .id is consistent with the type Foo (its type can be a subclass of Foo, but it's nothing new).

Here's the benefit: class Colors, instead of being declared as a class, may become static extension Colors on Color, preserving backward compatibility (almost -see *), and then we kill two birds with one stone: whatever was available via Colors.red is still available, plus - because all static constants defined in this extension are subclasses of dart:ui Color -they become available for shortcuts. In other words, now we have two possible prefixes for color red: backward-compatible Colors.red uses the extension name, and Color.red from static extension on Color, which makes the shortcut applicable to any parameter declared with the type Color.

(*) currently, class Colors defines a constructor Colors(), and a small number of instance methods like toString (inherited from Object), which cannot be ported to a static extension. I wonder if anyone really uses Colors() as a constructor (not clear what purpose it can serve).

Edit: some commenters on this and other designs are concerned about readability. Making a program readable is the responsibility of the user, not of the language. There're many ways to make the program unreadable with or without this feature.