Skip to content
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

Open
rami-a opened this issue May 16, 2019 · 306 comments
Open

Allow for shorter dot syntax to access enum values #357

rami-a opened this issue May 16, 2019 · 306 comments
Labels
enum-shorthands Issues related to the enum shorthands feature. enums feature Proposed language feature that solves one or more problems

Comments

@rami-a
Copy link

rami-a commented May 16, 2019

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
}
@johnsonmh
Copy link

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.

@kasperpeulen
Copy link

kasperpeulen commented May 18, 2019

In Swift this feature works not only for enums but also for static properties of classes. See also:

munificent/ui-as-code#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);

@lrhn
Copy link
Member

lrhn commented May 20, 2019

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.

@eernstg
Copy link
Member

eernstg commented May 20, 2019

One approach that could be used to avoid writing CompassPoint several times is a local import (#267).

@kasperpeulen
Copy link

How would the resolution work?

@lrhn You may want to study how it works in Swift. I think their implementation is fine.

@johnsonmh
Copy link

@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.

@lrhn
Copy link
Member

lrhn commented May 22, 2019

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.

@lrhn
Copy link
Member

lrhn commented Jun 24, 2019

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.

@ReinBentdal
Copy link

ReinBentdal commented Jun 24, 2019

To omit the . would make sense for widgets with constructors.

From

Text(
  'some text',
  style: FontStyle(
    fontWeight: FontWeight.bold
  ),
),

To

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

From issue #417

_Some pints may have been presented already

Not include subclasses of type

Invalid
padding: .all(10)

This wont work because the type EdgeInsetsGeometry is expected, but the type EdgeInsets which is a subclass is given.

Valid
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.

The ?. issue

Alot 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.

Other usecases

void weight(FontWeight fontWeight) {
  // do something
}
weight(.bold);

@andrewackerman
Copy link

@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.

@ReinBentdal
Copy link

A solution could be to introduce a identifyer.

*.bold // example symbol

But then again, that might just bloat the code/ language.

@lukepighetti
Copy link

lukepighetti commented Feb 27, 2020

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;
}

@MarcelGarus
Copy link
Contributor

MarcelGarus commented Jul 3, 2020

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),
        ),
      ],
    ),
  ],
);

@lrhn lrhn added the small-feature A small feature which is relatively cheap to implement. label Jul 8, 2020
@lrhn lrhn removed the small-feature A small feature which is relatively cheap to implement. label Sep 8, 2020
@munificent
Copy link
Member

munificent commented Sep 10, 2020

Replying to @mraleph's comment #1077 (comment) 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:

How does it interact with generics and type inference?

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.

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 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.

@Abion47
Copy link

Abion47 commented Sep 10, 2020

@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.

@mraleph
Copy link
Member

mraleph commented Sep 10, 2020

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.

@lukepighetti
Copy link

lukepighetti commented Sep 10, 2020

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.

@munificent
Copy link
Member

munificent commented Sep 10, 2020

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.

@tatumizer
Copy link

tatumizer commented Oct 9, 2024

Which feature, precisely?

I was talking about the feature that allows to say (in whatever syntactic form) "whenever the context type is AlignmentGeometry, you can use the shortcuts from the class Alignment". Your (hypothetical) example does exactly that!

static extension FromAlignment on AlignmentGeometry {
  static export Alignment;
}

I proposed a similar form before but didn't earn any points. I'm glad the idea finally got another supporter :-)
I, too, came to believe that show/hide is unnecessary. In case we have a name collision, the language will require disambiguation - otherwise your example won't work. (I understand that your example achieves more than the availability of shortcuts per se, but let's be honest: the feature is ALL about the shortcuts :-)

Do you agree that if we allow "static export" statement in the extension on AlignmentGeometry, we should also allow it in the class AlignmentGeometry itself, thus making the extension unnecessary in this specific case?

Yes, you can "static export" any number of classes, but in some common scenarios, it might be prone to misunderstanding.
(I can demonstrate the effect on examples later). Barring that, why not?

My question to you is: how did you arrive at the conclusion that "export" keyword is preferable to "import" in your example?
Can (or should) we support also the reverse case where class A says: I want all my static methods to be available in the class B?

(NOTE: The syntax of disambiguation is unclear. You can't say AlignmentGeometry.lerp because it will be ambiguous again).

@eernstg
Copy link
Member

eernstg commented Oct 10, 2024

For processing of shortcut syntax like .id, two things come up again and again:

  1. We must identify the namespace N where a static member named id or a constructor named N.id is looked up.
  2. The given namespace may be populated in multiple ways.

We could also consider execution ("Is this a compile-time only mechanism, or could it have a dynamic semantics which is independent of the static typing?"), but I won't go there because nobody (as far as I can see) has proposed that it should be anything other than a compile-time mechanism.


For (1), the most common proposal is to support situations where the context type is associated with a declaration that supports declarations of static members, and may support declarations of constructors (at least some kinds). We will then use the static namespace of that context type.

My proposal about default parameter scopes does this by default, but also allows an in N clause on a parameter declaration where the namespace N to search for shortcut names can be specified to be whatever the maintainer of that function wants.

We may encounter .id in a location where there is no context type (e.g., .id.m()). The most basic proposal would be that .id is only supported when there is a context type. A more common approach (which includes @lrhn's proposal and default parameter scopes, with various details differing a bit) is that the context type of .id.m() and similar member invocations (and member invocation chains, and cascades, and ...) is used when resolving .id as well.

We may of course have multiple syntactically nested expressions, and we could use a nested-scopes approach. For example, f(g(.id)) could be resolved by searching the shortcut namespace provided by g, and if nothing was found there we'd search the static namespace provided by f. Just like .id.m() might not have the type N (if that's even a type), there is no guarantee that .id is resolved as an expression whose type is assignable to the parameter type of g if we're using the static namespace for the parameter of f to resolve .id, but we will just generate code that has compile-time errors and then leave it to the developer to get the type right (so perhaps f(g(.id) needs to be edited to f(g(.id.m())) in order to be type correct).

I don't think we have any further proposals about (1).


For (2), the simplest approach is that the shortcut namespace is populated by actual syntax: When we have determined that the namespace N is the one to search for a particular shortcut .id, we proceed to look at the declarations in the body of N, exactly like we'd do if we encounter N.id in Dart code all the way back to Dart 1.0.

I've mentioned static extensions many times as an important mechanism that would allow developers to populate an namespace N, without having the ability to edit the declaration of N. If we're using a static extension S in this manner then (for code that imports or contains the static extension declaration) it works as if N were modified to contain those additional declarations in S. Crucially, this does not mean that the declaration of N now depends on S and on the dependencies of S, it's just the client who wants to use N with S that needs to import both of them.

@tatumizer proposed another mechanism that would populate a given static namespace: We could have import shortcut clauses in a static namespace declaration (that is, inside N). This mechanism would only enhance the given namespace when it is searched for shortcut names, the additions wouldn't occur for regular constructor invocations or static member invocations.

There were some ideas around including subtypes/subclasses as a way to automatically populate the namespace, e.g., for EdgeInsetsGeometry where we'd like to include .all as a shortcut for the constructor EdgeInsets.all. I couldn't find many details about this idea, but it could be explored.

The relevant design questions here would be at least the following:

  • Do we or do we not support injecting static members and/or constructors into an existing namespace?
  • Do static members/constructors thus injected exist for all kinds of lookup, or only for shortcuts? ... or in some other partial way?
  • What's the injection direction? That is, do we need to edit the declaration of N in order to import certain declarations from elsewhere, or is it possible to inject declarations into N without making any changes to N itself?
  • Do we have any kind of abstraction over members? (E.g., inject one at a time by name, inject all static members and/or constructors, inject a set which is tailored by show and/or hide, etc.)
  • Is it possible to inject a constructor of a different class N1 into a class declaration N? What does it mean, how is it called?
  • Is it possible for different clients to see different sets of members in a given static namespace N? Anything which is written inside N would presumably have the same effect for all clients; static extensions can be imported or not by each client. Is this ability to see different static members/constructors (and/or have different shortcuts) in each client desirable or not?

I think we have a bunch of jigsaw puzzle pieces here, and it's useful to try to organize them into a bigger picture.

@rrousselGit
Copy link

@eernstg

I think there's a slight misunderstanding about what my concerns are.

Cf:

They are not the "possible" values. They are the values which are conveniently expressible using an abbreviated syntax, that is, they are the "front row" of values.

With the current in T proposal, these are the possible values usable with the abbreviated syntax.
Your proposal does not support using the abbreviated syntax for values outside of in T, even if they are valid.

Given my in _Empty example, the goal isn't to make some code useless.
It is to show that in T behaves as an allowlist, not a "front row". Because it is currently impossible to bypass that in _Empty to add custom abbreviated values.

If a package author decided to, they could ban the abbreviated syntax from being used with their package.
I don't think that's healthy.

That's certainly an adjustment which could be made: Search the name of the shortcut in the static scope of the context type, as well as in the static scope of the in type. I don't know if it would be an improvement, but it's good to keep it in mind.

That seems very valuable.
I think that would massively reduce my issues with this in T feature.

Out of all the exchanges we've had, I'd say that's probably the first step in the right direction IMO :)

@tatumizer
Copy link

Do static members/constructors thus injected exist for all kinds of lookup, or only for shortcuts?

I will try to make an argument in favor of the latter - that is "only for shortcuts". So after injecting static members of Alignment into AlignmentGeometry, the constant topRight can be referred to as Alignment.topRight or (given a context type AlignmentGeometry) as .topRight, but never AlignmentGeometry.topRight. Thus, the notion of shortcut receives a first-class treatment rather than being a consequence of belonging to a namespace AlignmentGeometry.
Consider an example:

class Animal {
  //...
}
class Horse extends Animal {
  static final bolivar=Horse(name: 'Bolivar',...);
}

Would it be a good idea to copy the definition of bolivar to Animal so it could be referred to also as Animal.bolivar?

class Animal {
  static final bolivar=Horse.bolivar;
}

I don't think so. Citing O Henry, "Bolivar cannot carry double". We still can define a shortcut to Bolivar to be used with the context type Animal, but there's no reason to copy the reference to Bolivar to Animal class for that. The QN for Bolivar still remains Horse.bolivar, and it's never Animal.bolivar.

So my interpretation of export static Alignment would be: inject shortcuts from Alignment to the *set of shortcuts available for the context type AlignmentGeometry", rather than into a namespace, so better wording would be indeed "import shortcuts from Alignment"

As to whether the feature specifically dedicated to shortcuts makes sense, my answer is yes. It's a first-class, central feature of the design. If you open any example of Swift UI, you will see the shortcuts everywhere. Same would happen to Flutter apps.

@lrhn wrote:

That is, is this feature solely to support APIs that weren't designed with the shorthand feature in mind

There's no way to redesign Flutter with this specific feature in mind. The design of AlignmentGeometry with two subclasses is correct. (The Horse-Animal example shows that).

(We might want to discuss a better name for the concept. "Contextual shortcuts"? "Contextual aliases"? "Contexttual something"?

@eernstg
Copy link
Member

eernstg commented Oct 11, 2024

@rrousselGit wrote:

With the current in T proposal, these are the possible values usable with the abbreviated syntax.

That's certainly true. But note that you cannot write any abbreviated forms today, and every single actual argument which can be specified today can also be specified with the same syntax if we get this feature. In other words, there's nothing stopping you if you want to pass an actual argument that doesn't have an abbreviated form, and it isn't even going to get worse than what we have today.

So, "possible values usable with the abbreviated syntax" is not the same thing as "possible values".

Your proposal does not support using the abbreviated syntax for values outside of in T, even if they are valid.

The proposal that uses in clauses makes those expressions compile-time errors, which means that they are invalid by definition. 'Outside of T' is a big space, too, so I'm not sure how you'd define those additional valid values.

But I suppose it means something like "let us recognize .id where id is a static/constructor member of T or of the parameter type", and that is certainly a rule that we could use.

In general, the idea would be that you are primarily using the default (that is, the context type) to provide the namespace of static/constructor declarations which are available as shortcuts. This is probably going to work just fine in a lot of cases.

However, if the default won't work for you then you can use in T to specify another static namespace as the provider of shortcut declarations. This means that you are in control, you can make that namespace contain exactly the declarations you want.

This might occur because you want to support a very large number of shortcut names, and you don't want to pollute the static namespace of the target type itself; or you want to support different sets of shortcut names for different parameters (e.g., because the target type is used for many different purposes like int or Duration).

You can use mechanisms like static extensions to populate any of these namespaces (the default one in the target type itself, or any T that you have used in an in T clause), which will allow the declaration of the parameter (and thus possibly, the entire enclosing library) to be independent of the shortcut values themselves and their dependencies.

Hence, it makes sense that you could use an in clause like in MyNamespace where MyNamespace is an empty class, but this would presumably be because it is intended to be populated with a relevant set of shortcuts from some other library, probably because you want to avoid creating certain dependencies, or because you want to give the client full control over how to populate the namespace.

If you actually declare in _Empty as you mentioned (that is, using a private and empty class) then you have expressed the requirement that the set of shortcuts must be empty (because nobody can add to it from other libraries). This is probably not useful. It's similar to an infinite number of useless things that you can also do:

void foo(Never x) {
  ... // Lots of code that took days to write.
}

I just don't understand, what's the point you are trying to make by showing that it is possible to write a declaration which is useless, and perhaps even actively inconvenient? Of course you can do that!

Given my in _Empty example, the goal isn't to make some code useless.
It is to show that in T behaves as an allowlist, not a "front row". Because it is currently impossible to bypass that in _Empty to add custom abbreviated values.

Again, every actual argument which can be passed today can also be passed, using exactly the same syntax, if we add this feature. There's nothing non-allowed about an actual argument which isn't a shortcut.

If you want to enable shortcuts then change _Empty to Empty (and please give it a better name). Then document that developers can inject static members into Empty using a static extension, and that will give them the corresponding shortcuts.

If a package author decided to, they could ban the abbreviated syntax from being used with their package.
I don't think that's healthy.

Perhaps that package would then just be cloned, and the clone could be maintained by people who have better intentions?

... an adjustment which could be made: Search the name of the shortcut in the static scope of the context type, as well as in the static scope of the in type.

That seems very valuable

It wouldn't be hard, either.

However, it might be an improvement, and it might not. It certainly eliminates a little bit of expressive power. For example:

enum Day {monday, tuesday, wednesday, thursday, friday, saturday, sunday}

abstract final class WeekendDays { // Just a namespace.
  static const Day saturday = Day.saturday, sunday = Day.sunday;
}

void reportWeekendWork(int hours, Day day in WeekendDays) {
  ... // Report to my employer that I've been working during the weekend.
}

void main() {
  reportWeekendWork(2, .saturday); // OK.
  reportWeekendWork(5, .friday); // Compile-time error.
  reportWeekendWork(5, Day.friday); // OK, and this form is a hint that "I know what I'm doing".
}

Saturday and Sunday are the "front row" values, the typical values, for this particular purpose. But there could be special situations where work on other days is counted as weekend work as well. For instance, the weekend could be considered to start Friday afternoon, or you might want to report work after midnight on a Sunday specifying Day.monday as the day.

The point is that completion in an IDE will help you write .saturday and .sunday, but you have to use the non-abbreviated form if you want to choose a day which is not classified as "typical" for this purpose. This is convenient because it allows us to maintain a small and highly relevant set of completions. It also serves as a reminder (to authors and readers of the code) that the not-so typical values aren't typical, and perhaps they should be scrutinized a bit more by reviewers.

It makes sense to me that you might want to have subsets of an enumerated type as the available shortcuts, but this can't be expressed if we insist that all the static members of a class must be included as shortcuts.

@eernstg
Copy link
Member

eernstg commented Oct 11, 2024

I think there are several reasons why we might want more control over namespaces, for example in terms of an export static mechanism.

@tatumizer wrote:

after injecting static members of Alignment into AlignmentGeometry, the constant topRight can be referred to as Alignment.topRight or (given a context type AlignmentGeometry) as .topRight, but never AlignmentGeometry.topRight. Thus, the notion of shortcut receives a first-class treatment rather than being a consequence of belonging to a namespace AlignmentGeometry.

This would indeed eliminate the form AlignmentGeometry.topRight. By the way, so would this:

void foo(AlignmentGeometry alignment in AlignmentShortcuts) {...}

abstract final class AlignmentShortcuts {
  static export Alignment;
  static export AlignmentGeometry hide topRight; // Assume a name clash. We can resolve it.
}

The static export mechanism would have to be designed in detail, of course, but it seems easy to come up with a mechanism that exports static members.

However, the introduction of a constructor into a different namespace could gives rise to some confusion:

class A {
  const A.named();

  import shortcuts from B; // Imports constructors, too?
}

class B extends A {
  const B() : super.named() {...}
  factory B.otherName() => B();
}

void main() {
  A a1 = .named(); // OK, no problems.
  A a2 = .new(); // It may be confusing that it creates a `B()`.
  A a3 = .otherName(); // Probably OK.
}

So perhaps we shouldn't try to add constructors using these namespace management mechanisms? For example, adding the EdgeInsets.all constructor to the namespace of EdgeInsetsGeometry might be a bad idea, even for the limited purpose of providing shortcuts.

One way out would be to say that EdgeInsetsGeometry is irrelevant when it comes to shortcuts, we just want the ones in EdgeInsets:

void foo({required EdgeInsetsGeometry padding in EdgeInsets}) {...}

void main() {
  foo(padding: .all(16.0)); // Means `EdgeInsets.all(16.0)`.
  foo(padding: .lerp(someEdgeInset, anotherEdgeInset, 0.1); // OK.
  foo(padding: EdgeInsetsGeometry.lerp(...)); // No shortcuts for this one.
}

Another approach would be to equip the superclass (EdgeInsetsGeometry) with the desired constructors as forwarders:

void foo({required EdgeInsetsGeometry padding}) {...}

static extension on EdgeInsetsGeometry {
  const factory EdgeInsetsGeometry.all(double d) = EdgeInsets.all;
  const factory EdgeInsetsGeometry.directionalAll(double d) = EdgeInsetsDirectional.all;
  ... // More constructors as needed.
}

I know, this is considered verbose and inconvenient, but it is possible to do this using only static extensions, and it allows us to provide constructors from several subclasses, with renaming as needed.

@lrhn
Copy link
Member

lrhn commented Oct 11, 2024

There's no way to redesign Flutter with this specific feature in mind. The design of AlignmentGeometry with two subclasses is correct.

If there have to be two subclasses, then having to say which one you use seems reasonable. Not having a shortcut is working as intended?

But if, say, EdgeInsets.all and EdgeInsetsDirectional.all both work the same, I can't see why EdgeInsetsGeometry can't have a factory EdgeInsetsGeometry.all(double value) = EdgeInsets.all;. If all sides are the same, it doesn't matter if it's directional or not.
That might not work for AlignmentGeometry, and that's fine. You have to choose. There is no default. (If there was a default, it could be a constructor on AlignmentGeometry.)

@rrousselGit
Copy link

@eernstg
I don't think you've quite addressed what I was getting at.

I am fully aware that the option to use T.identifier instead of .identifier is always available. But that is unrelated to what I'm trying to say.

I want to evaluate the impacts of in T on the dot syntax, independently of anything else.
Specifically, my argument is that in T behaves like a Type constraint, for the dot syntax. This would there qualify it as "yet another type layer", and render it out of the scope of what the abbreviated syntax should do.

You said:

Your proposal does not support using the abbreviated syntax for values outside of in T, even if they are valid.

The proposal that uses in clauses makes those expressions compile-time errors, which means that they are invalid by definition. '

  • We agree that there is a compilation error. But a compilation-error can be many different things. What that error means can have fairly different ramifications.
    I'm trying to argue that the compilation error introduced by your proposal is similar to that of an "assignment error" when a type requirement isn't respected.

  • A "valid/invalid value" can have many different meaning.
    In your quote, your usage of "invalid value" implies: The Dart program would be considered invalid by the tooling. So that "value" cannot be used.
    When I'm saying your in T proposal rejects possibly valid values, I'm talking about how given fn(color: .red), if we changed the in T of color, that .red could be made to compile.

As thought experiment, consider the following code:

void fn({Color? color in *});

(In that scenario, we could have void fn({Color? color}) behave as void fn({Color? color in Color}).)

The idea of that thought experiment is, the meaning of in * would mean "Anything can be used as shortcuts".
This would be looking for static members of all classes/enums/...
That'd be the dynamic of the dot syntax.

Then, we are trying to compile this Dart expression: fn(color: .red).

From here, we could have the following algorithm:

    1. Lookup through all static variables accessible by the current library.
      Is that variable named red?
    • No? ignore it
    • Yes: collect it as possible target.
    1. How many possible targets have we collected?
    • 0: Missing identifier error
    • 2 or more: Name conflict error.
    • exactly 1: Keep going.
    1. Is this variable assignable to Color??
    • No: Assignment error
    • Yes: Done.

Then, we change in * to in T.
The algorithm we used before to handle .red using in * doesn't change. This only adds an extra filter somewhere between 1) and 2), to only include variables are indeed defined in T.

Afaik, this would be a fully working implementation of your in T proposal (with the added bonus of in * ; that's out of scope).

Where am I going with this?
The key is, by adding this in * feature, it should become apparent that in T doesn't behave as a requirement to help Dart figure out where to obtain values . Instead, it acts as a mechanism to exclude variables based off some properties (in this case, the location of where a variable is defined).

Added to that, that filter mechanism in T implements cannot be circumvented in any capacity.
It isn't like import ... show/hide, where there's always the option to import the same library again, using different modifiers.

So, in the context of the dot syntax, in T behaves like "yet another typing layer".

... an adjustment which could be made: Search the name of the shortcut in the static scope of the context type, as well as in the static scope of the in type.

That seems very valuable

It wouldn't be hard, either.

However, it might be an improvement, and it might not. It certainly eliminates a little bit of expressive power. For example:

The main appeal it has for me was, it reframed what in T means. It changed from an AND filter to an OR one.
This would guarantee that context types are always relevant, which is what I'm asking since the start.

It also means that adding in T on a parameter isn't a breaking change.

But looking back, the value is fairly low once we consider static extensions. We can safely ignore it.


To be a bit more productive here:

Do we need to implement that in T at the same time as the abbreviated syntax?
Couldn't namespace control for abbreviated syntax be implemented post initial release?

We are in agreement that void fn({Color color}) should use the context type by default.
In that situation, namespace control should be optional.

@tatumizer
Copy link

tatumizer commented Oct 11, 2024

@lrhn wrote:

If there have to be two subclasses, then having to say which one you use seems reasonable.

True. Unless you account for probabilities. BTW, I shared your view for a while, and still share it to some extent, but to a lesser extent than before. Again, there's a notion of a "typical use case".

We see this phenomenon in the notion of "default values". What is the point of them if you can always pass the parameter explicitly? And sometimes you must! But if there's a good default for a typical use case (which sometimes exists, sometimes doesn't), then it's a very convenient convention. The same happens here: in ClassName.parameterName, ClassName for a typical use case can be omitted.

Now it's a question of probabilities. It seems that in a large majority of cases, people use Alignment rather than AlignmentDirectional.. For many users who don't care about rtl alphabets, Alignment is all there is, and they will be very disappointed to find out that their assumed shortcuts are not really the shortcuts. The same happens with all classes whose name ends in Geometry (there's a number of them).
This observation has to be supported by stats, which I don't have. Also, there a possibility that Flutter team would veto the whole idea (I have no way to find out, but you do).

But if, say, EdgeInsets.all and EdgeInsetsDirectional.all both work the same, I can't see why EdgeInsetsGeometry can't have a factory EdgeInsetsGeometry.all(double value) = EdgeInsets.all;

They could do it, but they didn't. And I can understand (or, rather, speculate) why. The option "all" is just one of the possibilities. It's a part of a whole family of other possibilities, and they didn't want to promote one value, separating it from the rest of the family. (This makes sense to me).

But sometimes they DO promote. I'm aware of just a single example: lerp method. It can be found in both subclasses and in a base class too. There's a rumor that lerp from the base class can handle all functionality regardless of what the real class is (it allegedly relies on its own parameters while making a distinction). I haven't verified this claim, but the method is used rarely, so whether it is equipped with a shortcut or isn't or has an ambiguous shortcut requiring disambiguation - doesn't matter much in the large scheme of things :-)

@tatumizer
Copy link

tatumizer commented Oct 11, 2024

@eernstg : I agree that your example with named() could be confusing. The author of the code should keep the shortcuts in mind. Maybe there should be a way to fine-tune the set of shortcuts using show/hide clause. There's no harm in that IMO.

Speaking of "in" clause: I do believe that there's a good argument for it, in addition to the main mechanism of automatically exposed shortcuts from the namespace, plus with "export/import".
We must be able to write something like f(double foo with shortcuts {.high, .medium, .low}), but "in" doesn't resonate with me, though it is intended to mean the same. If we legalize the keyword shortcuts, we can write it as with shortcuts, which speaks for itself IMO. The cases for such parameter-specific shortcuts are rare, so a bit of extra verbosity is not an issue and it doesn't look like a separate type or something.

If we agree on with shortcuts, then, for symmetry, we can also write

static extension on AlignmentGeometry {
  with shortcuts in Alignment;
}

so there's no confusion around import/export.

My greater concern is that Flutter team may not approve this whole thing, and then all our discussions will be in vain :(

@tatumizer
Copy link

tatumizer commented Oct 11, 2024

Another observation: .new(...) is not a good shortcut for a default constructor. It should be just a dot: .(...). (In a sense, .new is worse than nothing).
Consider an example from flutter documentation (can be found in class Border API doc)

Container(
  decoration: const BoxDecoration(
    border: Border(
      top: BorderSide(color: Color(0xFFFFFFFF)),
      left: BorderSide(color: Color(0xFFFFFFFF)),
      right: BorderSide(),
      bottom: BorderSide(),
    ),
  ),
  child: Container(
    padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0),
    decoration: const BoxDecoration(
      border: Border(
        top: BorderSide(color: Color(0xFFDFDFDF)),
        left: BorderSide(color: Color(0xFFDFDFDF)),
        right: BorderSide(color: Color(0xFF7F7F7F)),
        bottom: BorderSide(color: Color(0xFF7F7F7F)),
      ),
      color: Color(0xFFBFBFBF),
    ),
    child: const Text(
      'OK',
      textAlign: TextAlign.center,
      style: TextStyle(color: Color(0xFF000000))
    ),
  ),
)

With .new replacing every constructor call, the picture is getting quite boring: all you can see is new, .new ad nauseum:

Container(
  decoration: const .new(
    border: .new(
      top: .new(color: .new(0xFFFFFFFF)),
      left: .new(color: .new(0xFFFFFFFF)),
      right: .new(),
      bottom: .new(),
    ),
  ),
  child: Container(
    padding: const .symmetric(horizontal: 20.0, vertical: 2.0),
    decoration: const .new(
      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))
    ),
  ),
)

Whereas with .(), we get it rather clean:

Container(
  decoration: const .(
    border: .(
      top: .(color: .(0xFFFFFFFF)),
      left: .(color: .(0xFFFFFFFF)),
      right: .(),
      bottom: .(),
    ),
  ),
  child: Container(
    padding: const .symmetric(horizontal: 20.0, vertical: 2.0),
    decoration: const .(
      border: .(
        top: .(color: .(0xFFDFDFDF)),
        left: .(color: .(0xFFDFDFDF)),
        right: .(color: .(0xFF7F7F7F)),
        bottom: .(color: .(0xFF7F7F7F)),
      ),
      color: .(0xFFBFBFBF),
    ),
    child: const Text(
      'OK',
      textAlign: .center,
      style: .(color: .(0xFF000000))
    ),
  ),
)

Another possibility is to disallow shortcuts to default constructors. Or to all constructors. Or to all methods except constants.
(The reason is: without the class names, it often might not be clear what the shortcut is doing).
Or maybe for methods and constructors, IDE should substitute full names. That is, when the user presses dot, show the list of all defined shortcuts, but for those of them that are not constants, substitute full names.


EDIT: For completeness, I include a variant based on <> prefix, as discussed elsewhere

Container(
  decoration: const <>(
    border: <>(
      top: <>(color: <>(0xFFFFFFFF)),
      left: <>(color: <>(0xFFFFFFFF)),
      right: <>(),
      bottom: <>(),
    ),
  ),
  child: Container(
    padding: const .symmetric(horizontal: 20.0, vertical: 2.0),
    decoration: const <>(
      border: .(
        top: <>(color: <>(0xFFDFDFDF)),
        left: <>(color: <>(0xFFDFDFDF)),
        right: <>(color: <>(0xFF7F7F7F)),
        bottom: <>(color: <>(0xFF7F7F7F)),
      ),
      color: <>(0xFFBFBFBF),
    ),
    child: const Text(
      'OK',
      textAlign: .center,
      style: <>(color: <>(0xFF000000))
    ),
  ),
)

@lrhn
Copy link
Member

lrhn commented Oct 12, 2024

Another possibility is to disallow shortcuts to default constructors. Or to all constructors. Or to all methods except constants.

That sounds like a lint.

If you don't like how the feature looks in a particular case, don't use it.
If you don't want it used in general, have a lint to complain if it gets used.

I'd let people use this as much as they want in their own code. We can't stop people from writing code with bad readability today, but if we don't allow an optional feature over hypothetical readability concerns, we may be preventing them from reading the best code in done cases.

(I won't support a lint about always or never using shorthands in the recommended lint set. That's a readability choice that depends on the actual code.)

@tatumizer
Copy link

tatumizer commented Oct 12, 2024

@lrhn: maybe we should look more closely at .(...)? I saw this syntax in another thread (#4124) where someone proposed it for pattern matching. I've just looked it up, and it turned out that the author of the idea was ... none other than yourself!
Quote:

I propose the syntax .(foo: var x).
That is, use . instead of _ to refer to the context type.
That matches the proposed .foo to refer to static members of the context type.

Indeed! (Further down the thread, I even tried to develop a theory of block expressions based on this syntax).

Why .(...) rather than .new(...)? The argument can be made that in declarative contexts, .new carries the wrong vibes.
In Flutter literals, we describe some arrangement of things. The emphasis there is not on the process of building the arrangement, but on the description of the result. new sounds as a verb here, but in Flutter literals, we are in the world of nouns. My speculation is that the syntax .(...) from your proposal for patterns is inspired by the same intuition - namely, that the pattern is a "static thing", so drawing attention to the process of creation of this "thing" with .new would be jarring. (Sorry if my interpretation of your motives is wrong :-).

At first, .(...) might look strange to a reader, but every notation looks strange if you've never seen it before, but as soon as you get used to it, you internalize it, and grow to embrace it with enthusiasm.

EDIT: I fixed my last post by adding a variant with <> syntax, which might indeed be a good candidate.

@eernstg
Copy link
Member

eernstg commented Oct 14, 2024

@rrousselGit wrote:

I want to evaluate the impacts of in T on the dot syntax, independently of anything else.

OK!

I don't think it's going to take us any further to discuss whether an in T clause on a parameter provides shortcuts for the static namespace of T, or it provides shortcuts for every static namespace of the entire reachable universe except that we must ignore everything that's not T.

So let's agree to disagree on the claim that 'in T works like "yet another typing layer"'.

However, I think it's important to note that the ability to have an in clause on a parameter makes it possible for the designer of the enclosing method/function to customize the set of shortcuts, as opposed to the situation where we always use the context type (such that the set of shortcuts is always exactly the static namespace of the parameter type).

Someone might say that we don't need this flexibility, we can just edit the class of the parameter type. However, nobody has the actual editing rights for every class in the universe, so that is generally not an option.

Next, a mechanism like static extensions could be used to add some static/constructor members to the parameter type (or indeed to any static namespace). That's a very important kind of abstraction (in particular, because it's modular, and it will allow us to eliminate certain kinds of unwanted dependencies). However, it can't be used to specify different sets of shortcuts for different parameters with the same parameter type. So static extensions are certainly useful and relevant, but they can't replace an in clause. Those two mechanisms are simply different, and they're serving different purposes.

In short, the support for in clauses adds expressive power, it doesn't take it away. It adds substantial expressive power in the sense that no other mechanism on the table subsumes it.

Of course, this expressive power includes the ability to specify that the set of shortcuts is empty (in _Empty), but if you don't want that outcome then I'd recommend that you don't do it.

When I'm saying your in T proposal rejects possibly valid values, I'm talking about how given fn(color: .red), if we changed the in T of color, that .red could be made to compile.

But this means that every shortcut .id is a possibly valid value, because I can just make it in MyNamespace and then write a fresh declaration named MyNamespace and put a static declaration in there whose name is id.

Perhaps you didn't mean "if we changed the in T of color (in whichever way you want)", but more specifically "if we changed the in T of color to Colors in 'material.dart', or to something similar which is declared in 'cupertino.dart'"?

But we can't have a language mechanism that relies on traversing all declarations that have a static namespace, and then selecting the "relevant" ones. There's nothing technical about Colors in 'material.dart' that makes it more relevant than MyNewNamespace above.

Do we need to implement that in T at the same time as the abbreviated syntax?
Couldn't namespace control for abbreviated syntax be implemented post initial release?

That's certainly true. If we introduce a mechanism that relies on the parameter type (only) then it is a non-breaking change to introduce support for a mechanism like in clauses later on, as long as the in clause is optional, and the meaning of an omitted in clause is to use the parameter type.

@jodinathan
Copy link

If we introduce a mechanism that relies on the parameter type (only) then it is a non-breaking change to introduce support for a mechanism like in clauses later on, as long as the in clause is optional, and the meaning of an omitted in clause is to use the parameter type.

This.
The dot feature as simple as syntax sugar is what we need.
It will save a lot of wasted time on having to open the property definition to check for the Type possibilities along with the wasted time on typing the type name.

@eernstg
Copy link
Member

eernstg commented Oct 14, 2024

@tatumizer wrote:

I agree that your example with named() could be confusing. The author of the code should keep the shortcuts in mind.

Yes, I tend to think that it is useful to ensure (by direct editing, or using static extensions, or in some other way) that the namespace which is being used has the declarations which are needed as shortcuts.

void foo({required A a}) {/*...*/}

class A {
  final int i;
  const A(this.i);
  
  static const a1 = A(1);
  static const a2 = A(2);
}

class B extends A {
  const B(super.i);
  void bar() {}
}

static extension AExtension on A {
  static const a3 = A(3);
  const factory A.asB(int i) = B;
}

void main() {
  // All shortcuts have the same expansion: Add `A` at the front.
  foo(a: .a1);
  foo(a: .a2);
  foo(a: .new(42));
  foo(a: .a3);
  foo(a: .asB(-10));

  // Non-shortcut expressions are of course still supported.
  foo(a: const B(14)); 
}

So we're introducing a constructor named A.asB that redirects to the constructor named B, rather than trying to make the constructor B available in the static namespace of A, somehow.

This means that there's no magic, there is nothing extra to explain, we just know that the relevant namespace is A, and hence .asB(-10) means A.asB(-10), and then we can apply the usual rules of the language to determine what A.asB means.

This is manual work (we need to declare the constructor A.asB, in the class A itself or in a static extension), and it does introduce a certain amount of redundancy (the parameter list of A.asB is a copy of the parameter list of B). Yet another issue is that A.asB(-10) has type A and B(-10) has type B; so there is a loss of typing precision.

However, I think the extra manual work is acceptable, given that we avoid having an extra mechanism that somehow implicitly "imports" the constructor B into the static namespace of A (and somehow renames it, because .new(-10) already means A.new(-10)). The less specific typing shouldn't matter much in practice because we're dealing with an expression which is passed as an actual argument with type A, so who cares that it is actually a B?

(OK, you can do foo(a: B(-10)..bar()), but not foo(a: .asB(-10)..bar()), but I still think that it doesn't matter that much. Just write foo(a: B(-10)..bar()) if that's what you want.)

Returning to the original comment:

I agree that your example with named() could be confusing.

.. I'd say that we should just make sure that there is no need for implicit imports of subclass constructors, we should simply make sure that there is a constructor which will do the job, even if that takes a little bit of manual work to create it.

"in" doesn't resonate with me
...
We must be able to write something like f(double foo with shortcuts {.high, .medium, .low})

It would certainly be a very easy change to use the syntax with shortcuts rather than in. We have had many discussions about brevity (including this entire issue!), so I'd be surprised if the longer form gets more support than the short one. But if we as a community are ready to embrace a more verbose form then it certainly seems useful to include a word like shortcuts.

I'm not so sure about listing the specific shortcuts. What if we want to provide a couple of thousand values? I'm also not quite sure where we'd go and look for a declaration named high. Perhaps we'd look in the static namespace of double? (We will probably use static extensions to put them in there.) Does this mean that everyone who just wants to use double as their library of shortcuts must live with the fact that it includes a shortcut named high (whose value presumably isn't relevant for most kinds of code)?

If we agree on with shortcuts, then, for symmetry, we can also write

static extension on AlignmentGeometry {
  with shortcuts in Alignment;
}

so there's no confusion around import/export.

Right, this is yet another example where it is very tempting to ask for namespace management features (import/export, hide/show, quantification).

I still prefer to focus on management of general static namespaces rather than management of namespaces specifically for shortcuts.

The point is that they are more general, and we can essentially express the latter using the former: Just declare a normal namespace N (say, an abstract final class), populate it with whatever you want to provide as shortcuts for a particular parameter (or parameters), and then use in N on the parameter or parameters where the given shortcuts should be enabled.

My greater concern is that Flutter team may not approve this whole thing

Surely we'll have something, perhaps just a plain "turn .id into C.id when the context type is C" mechanism at first. This doesn't mean that it is a waste of time to have a set of useful plans for how it could be generalized.

@rrousselGit
Copy link

@eernstg

I don't think it's going to take us any further to discuss whether an in T clause on a parameter provides shortcuts for the static namespace of T, or it provides shortcuts for every static namespace of the entire reachable universe except that we must ignore everything that's not T.

So let's agree to disagree on the claim that 'in T works like "yet another typing layer"'.

What's a typing layer according to you?

In the former scenario, in T acts as a source of information.
In the latter scenario, in T doesn't. We can locate values without it. It instead serves as a mechanism to have the compiler cause a compilation error when a shortcut doesn't respect the "is defined in T" type constraint. It is a form of assignment error.

And the purpose of in T as you've said yourself is clearly to reduce user mistakes ; which is exactly what types are for.

To top it all, it is a mechanism that is applied on pretty much every variable declaration, alongside its type.
It's not applied on a whole library, just one single parameter. It lives very next to anything that has a type. So much so that analyzer might want to have DartType include in T information because of how it'd be convenient to access and how they are closely related.

Someone might say that we don't need this flexibility, we can just edit the class of the parameter type. However, nobody has the actual editing rights for every class in the universe, so that is generally not an option.

We can make a wrapper class or use an extension types with anything.
That is always an option, even with types like int & co.

I don't think that specific problem is a concern of the dot syntax, but rather the typing system as a whole. And the solution doesn't have to be specific to the dot syntax.

The problem you're describing already exists today without this feature.
Many functions typically start with a bunch of assertions because they only accept a subset of the possible values defined by the type. There's a big overlap between such a function and the idea of "reducing the scope of possible shortcuts".

When I'm saying your in T proposal rejects possibly valid values, I'm talking about how given fn(color: .red), if we changed the in T of color, that .red could be made to compile.

But this means that every shortcut .id is a possibly valid value, because I can just make it in MyNamespace and then write a fresh declaration named MyNamespace and put a static declaration in there whose name is id.

That's what I'm saying, yes.
All static members named .id and with a matching type are IMO "valid values". A user could legitimately want to use any of them as shortcuts.

IMO, if a function takes in a Color, then a user should be able to define MyTheme.primaryColor and use it as fn(color: .primaryColor) ; without needing editing rights to the source of the function.

The only thing that matters to me is "What is necessary to get that .primaryColor to work?"
I don't really care what the solution is, as long as it's not:

  • "You can't use shortcut for that value and have to use MyTheme.primaryColor explicitly"
  • "You have to edit fn and possibly have to fork the package to do so"
  • "You can't because the package author of fn decided he didn't like shortcuts so he disabled them"

There can be some language limitation around it.
But I'm opposed to the idea of a package author having the final say on what can and cannot be used as shortcuts, when that feasible is purely sparkles for the eyes.

IMO what you're suggesting is no different from a function saying it can only work with multiline r""" ... """ to sometimes avoid mistakes.

@tatumizer
Copy link

tatumizer commented Oct 14, 2024

@eernstg

The method A.asB is not very useful IMO. First of all, it doesn't return B (which contradicts the semantics of the existing as B), and on top of that - what is the point? You would be better off by calling B() directly. (Not to mention that returning the wrong type will make chaining impossible without another conversion).

I am against injecting stuff into the namespace where it doesn't belong. It doesn't feel right. You can argue that the notation .id already suggests that the id is being taken from the namespace of the context type directly, regardless of whether we actually inject it or use a more specialized shortcut mechanism that only looks like this on the surface. That is a strong argument indeed. I don't have an answer to it. (Maybe we need a different notation?, Or maybe the user, after some initial confusion, can internalize the correct meaning of the leading dot? (see *))

I suggested with shortcuts in to make the two proposed syntactic forms uniform. The exact wording is the subject of debate, but the "verbosity" argument doesn't look strong. We cannot encode just everything using a limited set of available short words. The general principle here is that frequently used constructs must be short; but for rarely used things, we can afford a more verbose wording. (This is actually an idea of Huffman encoding). I don't think anyone will complain: in Flutter, I was able to find just a few examples of parameter-specific shortcuts; for static extensions and classes, "with" clause doesn't look too verbose to me.

(*) If we choose a special notation for SuggestedClass.id - e.g. S.id where S - some symbol, then there's a risk of the opposite effect: after a couple of days, the user might become annoyed with the prefix S.

@tatumizer
Copy link

tatumizer commented Oct 14, 2024

I think we can get away with pure "leading dot" notation by using a bit of linguistic equilibristics.
Let's forget the term "shortcut", and use the term "alias" instead. Then we can say that the notation .id always refers to an alias for some ClassName.id, according to the configuration of contextual aliases. The term "alias" conveys a more broad meaning than "belonging to a specific namespace".
When the user hovers over such an alias, IDE would show a real ClassName and otherwise behave as if we hovered over ClassName.id.

The convention may allow a dot without an id to serve as an alias, too, assuming it points to a default constructor of some class. We can also define an alias .foo that points to some ClassName.bar with no redirecting function.


A note about parameter-specific shortcuts: I tried to find the potential use cases in Flutter - they all deal with the parameters of type double; the constants of type double such that there's no other constants of type double are defined. In other words, if we have

class X {
   foo(double beauty) {...}
   //...
   static const highBeauty = 1.0;
   static const lowBeauty= 0.0;
   static const mediumBeauty = 0.5;
}

then there's no other "double" constants there.
This means that (apparently) we can declare foo as foo(double beauty with shortcuts in X) - with no need to use the set notation. I don't know whether this is some unstated design principle in Flutter, or I just failed to find the exceptions.
If such exceptions exist, you can indeed define an auxiliary type containing only the relevant constants (which is ugly, but will work).

@eernstg
Copy link
Member

eernstg commented Oct 15, 2024

@rrousselGit wrote:

What's a typing layer according to you?

I'll fold the response because I think it might not be of general interest.

I don't have any particular agenda with respect to the phrase 'typing layer'. I'd be perfectly happy if we'd just never use it.

Anyway, I'm trying to understand your intentions when using this phrase. My working hypothesis is that it means something like "a language mechanism that plays a role which is similar to the role played by a declared type".

"Similar" is a wonderfully flexible word, which means that almost anything can now be a 'typing layer'.

However, if we try to narrow it down to something that makes sense we could say that the mechanism must establish a guarantee. For example, if this 'typing layer' is used with a formal parameter then the body can make additional assumptions about the possible values of that parameter, because it's guaranteed that the value of the parameter will satisfy those assumptions.

With in T that's just not true, we don't know anything extra about the possible values given as actual arguments.

That's the main reason why I don't think it's reasonable to characterize in T as a typing mechanism.

The clause in T will do something concrete, though: it will enable resolution of terms of the form .identifier when passed as actual arguments to this parameter (if it's known statically that we are passing the argument to that particular parameter).

In other words, it enlarges the set of possible expressions that we can pass as actual arguments by a (small) set of terms of the form .identifier (plus .identifier(some, arguments) etc, depending on the features that are included for this mechanism).

In the former scenario, in T acts as a source of information.

Yes, it informs us about the fact that certain shortcuts are supported.

In the latter scenario, in T doesn't. We can locate values without it. It instead serves as a mechanism to have the compiler cause a compilation error when a shortcut doesn't respect the "is defined in T" type constraint. It is a form of assignment error.

I don't agree that this is similar to a type constraint, because knowing that a particular actual argument is also the value of a static member of T doesn't tell me anything which is normally considered to be a type. On top of this, we don't actually know this, because we can still pass all the actual arguments that the actual type constraint specifies (that is, the declared type of the parameter).

It sounds like you want to talk about the shortcuts as if we had a constraint like "thou must never pass any actual arguments that aren't shortcuts". I have no idea why anyone would want to enforce such a constraint.

And the purpose of in T as you've said yourself is clearly to reduce user mistakes ; which is exactly what types are for.

The purpose of in T is to enable a set of shortcuts.

If you wish to reduce user mistakes it might be helpful to specify a set of shortcuts which is narrow, consisting only of values that are definitely typical for this parameter. It might very well be a user mistake to pass a typical value as the actual argument in any given situation. Perhaps a particular call site requires an exceptionally rare value, because that's the correct thing to do. However, the big picture might still be that there will be fewer bugs in the invocations of the given method/function if the shortcuts are typically an appropriate argument value.

Types are there because it is helpful for overall program correctness to enforce that certain variables or parameters can only have values from a specific set (and expressions in general have similar guarantees). This means that we know the given value can be used in a specific manner (say, a String is known to have a method substring, so we can safely call it).

I don't think it's a useful perspective on shortcuts that they may help avoiding some errors because of their "typicality", and I do think it's useful to think about them as distinguished argument values that deserve a particularly convenient syntax; in T and any other mechanism for this issue adds shortcuts, they don't take away anything.

To top it all, it is a mechanism that is applied on pretty much every variable declaration, alongside its type.

That would be extremely surprising to me.

It's not applied on a whole library, just one single parameter. It lives very next to anything that has a type. So much so that analyzer might want to have DartType include in T information because of how it'd be convenient to access and how they are closely related.

Certainly, DartType might carry this information. No problem.

Someone might say that we don't need this flexibility, we can just edit the class of the parameter type. However, nobody has the actual editing rights for every class in the universe, so that is generally not an option.

We can make a wrapper class or use an extension types with anything.
That is always an option, even with types like int & co.

But you would never do that: A simple foo(42) would then have to be modified to foo(MyWrapperClass(42)). This would be massively breaking. Extension types would eliminate the run-time cost, but the call sites would still have to be modified.

I don't think that specific problem is a concern of the dot syntax, but rather the typing system as a whole. And the solution doesn't have to be specific to the dot syntax.

I was just saying that you can't actually add a new static member to, say, double. So we'd need something like static extensions or in clauses in order to be able to introduce the shortcut without editing double. Next, static extensions and in clauses do not have the same capabilities, so none of them can replace the other.

The problem you're describing already exists today without this feature.
Many functions typically start with a bunch of assertions because they only accept a subset of the possible values defined by the type. There's a big overlap between such a function and the idea of "reducing the scope of possible shortcuts".

You could also say that they are kind of opposite to each other: The functions with the assertions at the start of the body are (dynamically) enforcing some stronger constraints on the actual argument values than the declared type; a function using in clauses on one or more parameters are allowing some of the already supported actual argument values to be specified using a more concise syntax. The former doesn't introduce new syntax for any actual arguments, and the latter doesn't prevent any actual arguments from being passed. I see more contrast than overlap.

IMO, if a function takes in a Color, then a user should be able to define MyTheme.primaryColor and use it as fn(color: .primaryColor) ; without needing editing rights to the source of the function.

Indeed, that could be useful! This is a request for the kind of extensibility that I've proposed we should use static extensions to express:

// In 'some_lib.dart'.
void fn({required Color color}) {...}

// In 'main.dart'.
import 'some_lib.dart';

static extension on Color {
  static const primaryColor = MyTheme.primaryColor;
  ... // Other theme colors.
}

void main() {
  fn(color: .primaryColor); // OK.
}

If fn uses an in clause to specify some other namespace N then we'd need to use on N rather than on Color in the declaration of the static extension.

It would be highly convenient to have a kind of quantification (like static export MyTheme; which would work the same as the entire set of declarations above).

There could of course be name clashes. You could then use a different name in the static extension.

Other than that, I think it works.

The only thing that matters to me is "What is necessary to get that .primaryColor to work?"

Use a static extension to inject the desired shortcuts into the static namespace which is already chosen by fn as the location where shortcuts are found.

I don't really care what the solution is, as long as it's not:

  • "You can't use shortcut for that value and have to use MyTheme.primaryColor explicitly"
  • "You have to edit fn and possibly have to fork the package to do so"

You can use a shortcut, you don't have to edit fn or anything that it depends on.

  • "You can't because the package author of fn decided he didn't like shortcuts so he disabled them"

Please. The author of fn could also have chosen Never as the parameter type, disabling invocations. We don't generally spend time thinking about software designs which are actively written to be annoying or useless, and no Turing complete programming language will prevent such things.

But I'm opposed to the idea of a package author having the final say on what can and cannot be used as shortcuts, when that feasible is purely sparkles for the eyes.

You don't get to decide which parameters a given method of a class accepts, or their types, or the return type, or the name of the method. Similarly for getters, setters, variables. Similarly for lots of other properties that are part of a declaration. That's all fine, apparently.

There is one particular way to prevent the addition of extra shortcuts, namely: The package author made the choice to use in T where T is private. In that particular situation you can't add new shortcuts, because you can't denote that type (assuming you don't have edit rights to that library --- if you do have edit rights then you'd just change it to in SomeOtherDeclaration). Of all things, that's not fine? I'm not convinced that this would be a huge problem.

@tatumizer
Copy link

tatumizer commented Oct 15, 2024

Experiment: replacing .id with $.id, and .new(...) with $(...)
Now that the shortcuts don't start with the dot, there's no illusion that they belong to a namespace of the context type.

Look inside
Container(
  decoration: const $(
    border: $(
      top: $(color: $(0xFFFFFFFF)),
      left: $(color: $(0xFFFFFFFF)),
      right: $(),
      bottom: $(),
    ),
  ),
  child: Container(
    padding: const $.symmetric(horizontal: 20.0, vertical: 2.0),
    decoration: const $(
      border: $(
        top: $(color: $(0xFFDFDFDF)),
        left: $(color: $(0xFFDFDFDF)),
        right: $(color: $(0xFF7F7F7F)),
        bottom: $(color: $(0xFF7F7F7F)),
      ),
      color: $(0xFFBFBFBF),
    ),
    child: const Text(
      'OK',
      textAlign: $.center,
      style: $(color: $(0xFF000000))
    ),
  ),
)

The syntax $.id can be associated with the "default implementation" of an abstract class. The configuration might be as easy as:

abstract class AlignmentGeometry default Alignment {
  //...
}

This might cover many problematic cases that were not addressed by @lrhn's proposal.
For a "home namespace" of a context type, we can still use the .id syntax.
WDYT?

@rrousselGit
Copy link

rrousselGit commented Oct 15, 2024

Re: eernstg
I disagree with a many things here. But at that point our discussion isn't very productive I think. We clearly cannot convince each other for now.
The format of this back-and-forth probably doesn't help.

I'll just state again that it'd be worth looking into not including in T in the initial release of shortcuts.
The rest of the proposal doesn't seem very controversial to me.

@eernstg
Copy link
Member

eernstg commented Oct 16, 2024

@tatumizer wrote:

The method A.asB is not very useful IMO. First of all, it doesn't return B (which contradicts the semantics of the existing as B), and on top of that - what is the point?

I'm considering the situation where we want to provide shortcuts for multiple constructors declared by multiple classes (presumably, a set of classes that are all subtypes of the parameter type).

The typical example could be EdgeInsetsGeometry which is used as a parameter type where we might very well want to have shortcuts for constructors declared by the subclasses EdgeInsets and EdgeInsetsDirectional.

So how would we do that? A couple of obvious ideas are the following:

  1. Introduce a generalization of the notion of "the static namespace of a class C" such that it covers constructors (and other static members?) of (some or all?) subclasses of C. A variant of this proposal would be to use with shortcuts in C, and then generalize the provision of shortcuts by including subclasses of C etc.
  2. Generalize the in clause such that it can specify a set of static namespaces (foo({required EdgeInsetsGeometry padding in {EdgeInsets, EdgeInsetsDirectional}}) {...}). Name clashes could be handled in various ways, e.g., the first one wins, or we could have show/hide, etc.

I'm a little worried about the first one because it is quite "magic", that is, it will probably need to take a lot of decisions about which static members/constructors to include, from which classes, and it isn't obvious that the developer can do anything if those choices aren't as desired. Alternatively, we'd need to introduce machinery such that the developer can make the choices as needed.

The second alternative would be rather straightforward. We probably still don't want to have a huge amount of machinery, at least not unless we introduce a separate declaration such that it doesn't swamp the parameter declaration entirely:

shortcut MyShortcut = {
  EdgeInsetsGeometry hide new,
  EdgeInsets, // A plain `.new(...)` means `EdgeInsets(...)`, and we get `.all` etc as well.
  EdgeInsetsDirectional rename all as directionalAll,
};

void foo({required EdgeInsetsGeometry padding in MyShortcut}) {...}

In any case, we would probably have a mechanism which is too magic, or we'd introduce a lot of machinery in order to be able to control all that magic as needed.

Now, the motivation for introducing declarations like A.asB is that it exemplifies a third strategy that avoids adding a lot of new machinery:

  1. Use existing mechanisms to set up a static namespace whose contents is exactly the desired shortcuts.

We could then use EdgeInsetsGeometry as the static namespace (that is, there will probably not be any in clauses, we will just use the default for all parameters with this type), and we would use static extensions to populate this static namespace.

If we don't want to use a new, specialized mechanism which will allow us to inject an EdgeInsets constructor into the namespace of EdgeInsetsGeometry then we can actually use an existing mechanism. Namely, a redirecting factory constructor.

// --- In Flutter libraries.

abstract class EdgeInsetsGeometry {
  const EdgeInsetsGeometry();
}

class EdgeInsets extends EdgeInsetsGeometry {
  ...
}

class EdgeInsetsDirectional extends EdgeInsetsGeometry {
  ...
}

// --- 'my_lib.dart'.

static extension EdgeInsetsGeometryExtension on EdgeInsetsGeometry {
  factory EdgeInsetsGeometry.all(double _) = EdgeInsets.all;
  factory EdgeInsetsGeometry.directionalAll(double _) = EdgeInsetsDirectional.all;
  ... // Other constructors and static members that we want to use as shortcuts.
}

void foo({required EdgeInsetsGeometry padding}) {...}

// --- 'main.dart'.

void main() {
  foo(padding: .all(16.0));
  foo(padding: .directionalAll(16.0));
  ...
}

The trade-off is that (a) there's no need for new, elaborate mechanisms in order to populate EdgeInsetsGeometry with constructors that create instances of subclasses, we already have redirecting factories for that, and (b) this technique is manual and verbose, but we can use it if needed, and it supports precise control including renaming.

I am against injecting stuff into the namespace where it doesn't belong.

Agreed, but I don't think it applies here. It is a perfectly well-known and legitimate design that a given (perhaps abstract) class C contains redirecting constructors such that clients can create instances of various classes from the entire hierarchy under C, and developers don't need (or want) to know that some constructors will return instances of proper subtypes of C, they will just be treated as C instances anyway.

From this point of view it is also not a (big?) problem that .directionalAll(16.0) has static type EdgeInsetsGeometry, even though we could actually write EdgeInsetsDirectional.all(16.0) and obtain the exact same object, only with the more special type EdgeInsetsDirectional.

It all depends. If EdgeInsetsDirectional has a different (bigger) interface than EdgeInsetsGeometry then we probably don't want to do this, but if they all have the same interface then I can't see a big problem.

@kallentu kallentu added the enum-shorthands Issues related to the enum shorthands feature. label Oct 16, 2024
@tatumizer
Copy link

tatumizer commented Oct 16, 2024

@eernstg wrote:

If EdgeInsetsDirectional has a different (bigger) interface than EdgeInsetsGeometry then we probably don't want to do this

They all have different interfaces.

I am thinking of another idea.

  1. Let's not mess with the home namespace of "context type". It was designed by Flutter people, with a certain philosophy in mind.
    (Especially given that we practically agreed that adding stuff where it doesn't belong was not a good idea)

  2. Let's denote a reference to a context type as _, as discussed in a parallel thread. Then _.id is allowed to be written as .id; constructor call _.new(...) - as _(...). I don't think these conventions are controversial.

  3. For "other stuff" we want to call using a shortcut notation, let's have another namespace (just one!). We need a special syntax to refer to the symbols from this "other namespace". I suggest $.id, with the $.new(...) supporting the abbreviation $(...).
    In Flutter, the default constructors are very common - see "Look inside" in the previous post for an example.
    Important: in this "other namespace", we don't lose type information. (if you know a better symbol than $ - please suggest; for me, it looks acceptable: the syntax $.id is familiar to javascript programmers).

  4. How to populate this "another namespace"? How to name it? These are the open issues, but they are much easier than the original problem.

The idea of "another namespace" hinges on the assumption that all (or most) problematic cases for shorthand notation are exactly those that come from the inheritance; in Flutter, hierarchies are shallow, with the (abstract) base class A and its direct subclasses B, C, ... (in most cases there are only two), and it would be possible to choose one of them to serve as a kind of "default implementation", or "most commonly used implementation" of A. Then the $ will have a very concrete meaning (e.g. B).
If we prove that by making this choice we cover the majority of use cases, then we can for simplicity adopt this view as a definition of "another namespace". In other words, $ refers to a default implementation of the context type.

@eernstg
Copy link
Member

eernstg commented Oct 17, 2024

Let's denote a reference to a context type as _, as discussed in a parallel thread. Then _.id is allowed to be written as .id; constructor call _.new(...) - as _(...). I don't think these conventions are controversial.

Interesting! I like the consistency part of this idea: "_ means the same thing here as in ...(some other feature)...", even if it's only "roughly".

We have two mechanisms here: (1) _ stands for a type which is obtained implicitly according to the location where it occurs (most likely, using the context type for that location, or for some bigger expression that contains it). (2) .id stands for T.id where T is obtained similarly.

The incompleteness is visible: A reader would know that _ in the given location will be implicitly replaced by a type, and . will be prepended by a type. It's a nice observation that we could use _.id, and reduce this to a single mechanism.

On the other hand, if _.id will always be written as .id then perhaps it isn't very helpful to explain what's going on in terms of two abbreviation mechanisms. ;-) I'm also a little worried about the fact that an instance creation can have so many different forms: EdgeInsets.all(16.0), _.all(16.0), .all(16.0). Assuming a context type of C and a constructor named C, I'm also wondering about the readability of _(15) vs. .new(15). The .new part does seem helpful to me.

Anyway, it's great to have so many ideas on the table!

@tatumizer
Copy link

tatumizer commented Oct 17, 2024

I'm also a little worried about the fact that an instance creation can have so many different forms: EdgeInsets.all(16.0), _.all(16.0), .all(16.0)

Yes, I am also of two minds about abbreviating _.id to .id.
How much will we gain from such an abbreviation?
This depends on how often (in % terms) such .id abbreviations can be helpful in a real-life programs, especially in Flutter.

The cases of calling default constructors are very common in Flutter - this is visible to the naked eye, so _(...) is justified, but .new(...) is too long (and too distracting) for an operation that occurs on every line of the code, sometimes several times on the same line.
From the same experiments, I can see that the constants like .topRight are not that common, so writing it as _.topRight (with an extra underscore) won't be a big deal.
More statistical experiments are needed, but you need a script for this. Can someone write a script? 😄

(My working hypothesis is that from UX standpoint _.id is strictly better than .id: it's more readable; while typing, pressing leading _ gets you the same suggestions you would otherwise associate with .. I speculate that if not for swift, we would never even start discussing the .id shortcut syntax)

@mateusfccp
Copy link
Contributor

(My working hypothesis is that from UX standpoint _.id is strictly better than .id: it's more readable; while typing, pressing leading _ gets you the same suggestions you would otherwise associate with .. I speculate that if not for swift, we would never even start discussing the .id shortcut syntax)

I don't see how _.id is more readable than .id. I agree that the idea of unifying the concepts is appealing, though.

@tatumizer
Copy link

@mateusfccp : if we ever get the constructor invocation like _(...) supported, then using the underscore in _.id is consistent accross the board; otherwise, you need an additional level of "mental redirection": .id -> _.id -> ContextType.id. Keeping things uniform helps.
In chains, A a = _.foo.bar.baz(). the leading underscore makes things less magic IMO.
(This is all subjective though, you might disagree).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enum-shorthands Issues related to the enum shorthands feature. enums feature Proposed language feature that solves one or more problems
Projects
Status: Being spec'ed
Development

No branches or pull requests