dart-lang / language

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

Parameter default scopes #3834

Open eernstg opened 1 month ago

eernstg commented 1 month ago

In response to https://github.com/dart-lang/language/issues/357:

Here is an idea that the language team members have discussed previously, but so far it does not seem to have an issue where it is spelled out in any detail.

It supports concise references to enum values (e.g., f(mainAxisAlignment: .center) and case .center: rather than f(mainAxisAlignment: MainaxisAlignment.center) and case MainAxisAlignment.center:), and it supports similarly concise invocations of static members and constructors of declarations that may not be enums. The leading period serves as a visible indication that this feature is being used (that is, we aren't using normal scope rules to find center when we encounter .center).

Introduction

We allow a formal parameter to specify a default scope, indicating where to look up identifiers when the identifier is prefixed by a period, as in .id.

We also allow a switch statement and a switch expression to have a similar specification of default scopes.

Finally, we use the context type to find a default scope, if no other rule applies.

The main motivation for a mechanism like this is that it allows distinguished values to be denoted concisely at locations where they are considered particularly relevant.

The mechanism is extensible, assuming that we introduce support for static extensions. Finally, it allows the context type and the default scope to be decoupled; this means that we can specify a set of declarations that are particularly relevant for the given parameter or switch, we aren't forced to use everything which is specified for that type.

The syntax in E is used to specify the default scope E. For example, we can specify that a value of an enum type E can be obtained by looking up a static declaration in E:

enum E { e1, e2 }

void f({E e in E}) {}

void g(E e) {}

void main() {
  // Using the default scope clause `in E` that `f` declares for its parameter.
  f(e: E.e1); // Invocation as we do it today.
  f(e: .e1); // `.e1` is transformed into `E.e1`: `.` means that `e1` must be found in `E`.

  // Using the context type.
  E someE = .e2;
  g(.e1);

  // A couple of non-examples.
  (f as dynamic)(e: .e1); // A compile-time error, `dynamic` does not provide an `e1`.
  Enum myEnum = .e2; // A compile-time error, same kind of reason.
}

It has been argued that we should use the syntax T param default in S rather than T param in S because the meaning of in S is that S is a scope which will be searched whenever the actual argument passed to param triggers the mechanism (as described below). This proposal is written using in S because of the emphasis on conciseness in many recent language developments.

If a leading dot is included at the call site then the default scope is the only scope where the given identifier can be resolved. This is used in the invocation f(e: .e1).

The use of a default scope is especially likely to be useful in the case where the declared type is an enumerated type. For that reason, when the type of a formal parameter or switch scrutinee is an enumerated type E, and when that formal parameter or switch does not have default scope, a default scope clause of the form in E will implicitly be induced. For example:

enum E { e1, e2 }

void main() {
  var x = switch (E.e1) {
    .e1 => 10,
    .e2 => 20,
  };
}

We can support looking up colors in Colors rather than Color because the in E clause allows us to specify the scope to search explicitly:

void f(Color c in Colors) {}

void main() {
  f(.yellow); // OK, means `f(Colors.yellow)`.
}

Assuming that a mechanism like static extensions is added to the language then we can add extra colors to this scope without having the opportunity to edit Colors itself:

static extension MyColors on Colors {
  static const myColor = Colors.blue;
}

void main() {
  f(.myColor); // OK, means `f(Colors.myColor)`, aka `f(MyColors.myColor)`.
}

We can also choose to use a completely different set of values as the contents of the default scope. For example:

class AcmeColors {
  static const yellow = ...;
  ... // Lots of colors, yielding a suitable palette for the Acme App.
  static const defaultColor = ...;
}

class MyAcmeWidget ... {
  MyAcmeWidget({Color color = defaultColor in AcmeColors ...}) ...
}

...
build(Context context) {
  var myWidget = MyWidget(color: .yellow); // Yields that very special Acme Yellow.
}
...

This means that we can use a standard set of colors (that we can find in Colors), but we can also choose to use a specialized set of colors (like AcmeColors), thus giving developers easy access to a set of relevant values.

If for some reason we must deviate from the recommended set of colors then we can always just specify the desired color in full: MyAcmeWidget(color: Colors.yellow ...). The point is that we don't have to pollute the locally available set of names with a huge set of colors that covers the needs of the entire world, we can choose to use a more fine tuned set of values which is deemed appropriate for this particular purpose.

This is particularly important in the case where the declared type is widely used. For instance, int.

extension MagicNumbers on Never { // An extension on `Never`: Just a namespace.
  static const theBestNumber = 42;
  static const aBigNumber = 1000000;
  static const aNegativeNumber = -273;
}

void f(int number in MagicNumbers) {...}

void main() {
  f(.theBestNumber); // Means `f(42)`.
  f(14); // OK.

  int i = 0;
  f(i); // Also OK.
}

This feature allows us to specify a set of int values which are considered particularly relevant to invocations of f, and give them names such that the code that calls f will be easier to understand.

We can't edit the int class, which implies that we can't use a mechanism that directly and unconditionally uses the context type to provide access to such a parameter specific set of names.

We could use static extensions, but that doesn't scale up: We just need to call some other function g that also receives an argument of type int and wants to introduce symbolic names for some special values. Already at that point we can't see whether any of the values was intended to be an argument which is passed to f or to g.

// Values that are intended to be used as actual arguments to `f`.
static extension on int {
  static const theBestNumber = 42;
  static const aBigNumber = 1000000;
  static const aNegativeNumber = -273;
}

// Values that are intended to be used as actual arguments to `g`.
static extension on int {
  static const theVeryBestNumber = 43;
}

// A mechanism that relies on the context type would work like a
// default scope which is always of the form `T parm in T`.
void f(int number in int) {...}
void g(int number in int) {...}

void main() {
  f(theBestNumber); // OK.
  g(theBestNumber); // Oops, should be `theVeryBestNumber`.
}

Proposal

Syntax

<normalFormalParameter> ::= // Modified rule.
    <metadata> <normalFormalParameterNoMetadata> <defaultScope>?

<defaultNamedParameter> ::= // Modified rule.
    <metadata> 'required'? <normalFormalParameterNoMetadata>
    ('=' <expression>)? <defaultScope>?

<defaultScope> ::= 'in' <namedType>
<namedType> ::= <typeIdentifier> ('.' <typeIdentifier>)?

<primary> ::= // Add one alternative at the end.
    :    ...
    |    '.' <identifierOrNew>

<switchExpression> ::=
    'switch' '(' <expression> ')' <defaultScope>?
    '{' <switchExpressionCase> (',' <switchExpressionCase>)* ','? '}'

<switchStatement> ::=
    'switch' '(' <expression> ')' <defaultScope>?
    '{' <switchStatementCase>* <switchStatementDefault>? '}'

Static analysis

This feature is a source code transformation that transforms a sequence of a period followed by an identifier, .id, into a term of the form E.id, where E resolves to a declaration.

The feature has two parts: An extra clause known as a default scope clause which can be specified for a formal parameter declaration or a switch statement or a switch expression, and a usage of the information in this clause at a call site (for the formal parameter) respectively at a case (of the switch).

The syntactic form of a default scope clause is in E.

A compile-time error occurs if a default scope contains an E which does not denote a class, a mixin class, a mixin, an extension type, or an extension. These are the kinds of declarations that are capable of declaring static members and/or constructors.

The static namespace of a default scope clause in E is a mapping that maps the name n to the declaration denoted by E.n for each name n such that E declares a static member named n.

The constructor namespace of a default scope clause in E is a mapping that maps n to the constructor declaration denoted by E.n for each name n such that there exists such a constructor; moreover, it maps new to a constructor declaration denoted by E, if it exists (note that E.new(); also declares a constructor whose name is E).

Consider an actual argument .id of the form '.' <identifier> which is passed to a formal parameter whose statically known declaration has the default scope clause in E.

Assume that the static or constructor namespace of in E maps id to a declaration named id. In this case id is replaced by E.id.

Otherwise, a compile-time error occurs (unknown identifier).

In short, an expression of the form .id implies that id is looked up in a default scope.

Consider an actual argument of the form .id(args) where id is an identifier and args is an actual argument list.

If neither the static nor the constructor namespace contains a binding of id then a compile-time error occurs (unknown identifier).

Otherwise, .id(args) is transformed into E.id(args).

Consider an actual argument of the form .id<tyArgs>(args) where id is an identifier, tyArgs is an actual type argument list, and args is an actual argument list.

If neither the static nor the constructor namespace contains a binding of id then a compile-time error occurs (unknown identifier). If the constructor namespace contains a binding of id, and the static namespace does not, then a compile-time error occurs (misplaced actual type arguments for a constructor invocation).

Otherwise, .id<tyArgs>(args) is transformed into E.id<tyArgs>(args).

Note that it is impossible to use the abbreviated form in the case where actual type arguments must be passed to a constructor. We can add syntax to support this case later, if desired.

class A<X> {
  A.named(X x);
}

void f<Y>(A<Y> a) {}

void main() {
  // Assume that we want the type argument of `f` to be `num`, and the type argument
  // to the constructor to be `int`.
  f<num>(A<int>.named(42)); // Using the current language, specifying everything.
  f<num>(<int>.named(42)); // Syntax error.
  f<num>(.named<int>(42)); // Wrong placement of actual type arguments.
  f<num>(.named(42)); // Allowed, but the constructor now gets the type argument `num`.
}

We generalize this feature to allow chains of member invocations and cascades:

Let e be an expression of one of the forms specified above, or a form covered by this rule. An expression of the form e s where s is derived from <selector> will then be transformed into e1 s if e will be transformed into e1 according to the rules above.

The phrase "a form covered by this rule" allows for recursion, i.e., we can have any number of selectors.

Let e be an expression of one of the forms specified above. An expression of the form e .. s or e ?.. s which is derived from <cascade> will then be transformed into e1 .. s respectively e1 ?.. s if e will be transformed into e1 according to the rules above.

The resulting expression is subject to normal static analysis. For example, E.id<tyArgs>(args) could have actual type arguments that do not satisfy the bounds, or we could try to pass a wrong number of args, etc.

This feature is implicitly induced in some cases:

It is recommended that the last clause gives rise to a warning in the situation where said context type is the result of promotion, or it's the result of type inference.

Dynamic semantics

This feature is specified in terms of a source code transformation (described in the previous section). When that transformation has been completed, the resulting program does not use this feature. Hence, the feature has no separate dynamic semantics.

Versions

eernstg commented 1 month ago

Checking this proposal against the cases in this comment.

The main issue to discuss here is probably that we will fix at the declaration of each formal parameter that supports this kind of abbreviation from which scope it can be made available.

For example, there is a case below where a member has type EdgeInsetsGeometry, but the actual argument has type EdgeInsets. I've addressed that by including support for both of those scopes, but it gets harder if we wish to enable many scopes.

A counter point would be that we can add static extensions to the language, and this would allow us to add extra members to existing scopes.

Enums

Example 1: BoxFit

Use current:

Image(
  image: collectible.icon,
  fit: BoxFit.contain,
)

Use with this proposal:

Image(
  image: collectible.icon,
  fit: .contain,
)

Definitions:

class Image extends StatefulWidget {
  final BoxFit? fit;

  const Image({
    super.key,
    required this.image,
    ...
    this.fit,
  });
}

enum BoxFit {
  fill,
  contain,
  ...
}

Example 2: Alignment

Use current:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  mainAxisSize: MainAxisSize.min,
  children: [ ... ],
)

Use with this proposal:

Row(
  mainAxisAlignment: .center,
  mainAxisSize: .min,
  children: [ ... ],
)

Definitions:

class Row extends Flex {
  const Row({
    ...
    super.mainAxisAlignment,
    ...
  }) : super(
    ...
  );
}

class Flex extends MultiChildRenderObjectWidget {
  final MainAxisAlignment mainAxisAlignment;

  const Flex({
    ...
    this.mainAxisAlignment = MainAxisAlignment.start,
    ...
  }) : ...
}

enum MainAxisAlignment {
  start,
  end,
  center,
  ...
}

Named constructors

Example 1: BackdropFilter

Use current:

BackdropFilter(
  filter: ImageFilter.blur(sigmaX: x, sigmaY: y),
  child: myWidget,
)

Use with this proposal:

BackdropFilter(
  filter: .blur(sigmaX: x, sigmaY: y),
  child: myWidget,
)

Definitions:

class BackdropFilter extends SingleChildRenderObjectWidget {
  final ui.ImageFilter filter;

  const BackdropFilter({
    required this.filter in ui.ImageFilter,
    ...
  });
}

abstract class ImageFilter {
  ImageFilter._(); // ignore: unused_element
  factory ImageFilter.blur({
    double sigmaX = 0.0,
    double sigmaY = 0.0,
    TileMode tileMode = TileMode.clamp,
  }) { ... }
}

Example 2: Padding

Use current:

Padding(
  padding: EdgeInsets.all(32.0),
  child: myWidget,
),

Use with this proposal:

Padding(
  padding: .all(32.0),
  child: myWidget,
),

Definitions:

class Padding extends SingleChildRenderObjectWidget {
  final EdgeInsetsGeometry padding;

  const Padding({
    super.key,
    required this.padding in EdgeInsets,
    super.child,
  });
}

class EdgeInsets extends EdgeInsetsGeometry {
  ...
  const EdgeInsets.all(double value)
   : left = value,
      top = value,
      right = value,
      bottom = value;
}

Static members

Use current:

Icon(
  Icons.audiotrack,
  color: Colors.green,
  size: 30.0,
),

Use with this proposal:

Icon(
  .audiotrack,
  color: green,
  size: 30.0,
),

Definitions:

class Icon extends StatelessWidget {
  /// Creates an icon.
  const Icon(
    this.icon in Icons, {
    ...
    super.color in Colors, // Or whatever the default scope of colors is called.
  }) : ... ;

  final IconData? icon;
}

abstract final class Icons {
  ...
  static const IconData audiotrack = IconData(0xe0b6, fontFamily: 'MaterialIcons');
  ...
}
rrousselGit commented 1 month ago

To me the fact that functions have to explicitly opt-in to this is a deal breaker.

It is going to be extremely frustrating to have to add this in Type in all parameters of the public API of a package. And users are bound to be frustrated when they want to use the shorthand, but a parameter did not specify in Type.

It also hard-codes those short-hands in the package ; when users may want to define their own shorthands. A typical example: Colors/Icons. Folks will want to define shortcuts for their primary colors or app icons. But Flutter would have a hard-coded in Colors, so this wouldn't work.

Last but not least, there's also the case of generics:

void fn<T>(T value);

It is unclear to me how we could handle fn<Color>(Colors.red) here.

eernstg commented 1 month ago

To me the fact that functions have to explicitly opt-in to this is a deal breaker.

Good points! Let me try to soften them a bit.

It is going to be extremely frustrating to have to add this in Type in all parameters of the public API of a package.

True, that could give rise to a substantial amount of editing.

We could have some amount of tool support.

For example, I'd expect enumerated types to give rise to the vast majority of usages of this mechanism. This is a good match because there's no doubt that we will have to provide one of the values of that particular enumerated type, so we're always going to get a shorthand for precisely the values that are relevant. So we should probably have a quick fix for any parameter whose type is an enumerated type E, adding in E.

Next, the mechanism could be introduced gradually for any other usages. For example, adding support for blur and other ImageFilter constructors could be done for parameters of that type, and call sites in new code could then be less verbose than existing call sites.

It also hard-codes those short-hands in the package

I expect this mechanism to play well together with a static extension mechanism. So if you want to have your own extended set of colors you would add them to Colors, rather than creating a new entity (that the parameter does not know anything about). Search for MyColors in the initial posting in order to see an example.

This makes a specification like Color c in Colors extensible in a scoped manner. That is, you can have your own extra colors in a static extension of Colors, and other folks could have their own extra colors similarly, and they would exist at the same time without creating any conflicts, even if both of you want to use Colors.crimson with a different meaning, because each of you would import one of those static extensions, not both.

Finally, for the generic case:

void fn<T>(T value);

For the invocation fn<Color>(Colors.red) there wouldn't be any support for an abbreviation, you will just have to write it in full. We might be able to come up with something really fancy, but for now I think it's OK.

I think the danger associated with a very broad mechanism that would enable red to be transformed into Colors.red in a very large number of locations (like, "in every location where the context type is Color") is more serious than the convenience of being able to cover cases like fn<Color>(red) can justify. This is particularly true because the type argument which is passed to fn is probably going to be inferred, not explicit.

cedvdb commented 1 month ago

This could be implied and the default

enum E { e1, e2 }

void f({E e in E}) {     // unnecessary in E

Which would be the same as

enum E { e1, e2 }

void f({E e}) {
eernstg commented 1 month ago

This could be implied

True! I don't know if that would be too aggressive. Maybe ... perhaps ... it would be OK to say that this mechanism is always enabled implicitly for parameters whose type is an enum. On the other hand, that would immediately call for a way to opt out. We could use something like in Never to indicate that the abbreviation should not be used at all. In any case, that's fine tuning and we can easily make adjustments like that if it turns out to be desirable.

cedvdb commented 1 month ago

@eernstg I believe your example is not what you meant to write in static members color: Colors.green should be green.

imo, keep the dot . in front of the shorthand, it's more readable

jakemac53 commented 1 month ago

On the other hand, that would immediately call for a way to opt out.

Out of curiosity, why? At least for the author of an API, they should not care how the parameters are passed syntactically, only that the values that are coming in are of the expected type?

If anything, users might want to be able to opt out, but I don't know how that would work.

jakemac53 commented 1 month ago

so no "in" introduction for now.

I agree that in seems unnecessary, especially if we get static extensions. I think it is better if the person invoking the function, not the API designer, controls which things can be passed using this shorthand.

That makes me think, what if we just had a more general feature to add static members into the top level scope?

As a total straw man:

import 'package:flutter/material.dart' with Colors; // All the static members on Colors are now in the top level scope 

That I think is possibly a simpler feature, and puts all the control in the users hands? And at least you don't have to repeat the class name multiple times in a library. Maybe you could even export the static scope like this as a top level scope, so you could have a utility import which does this by default.

Reprevise commented 1 month ago

I like the idea of being able to import things into the top level scope. In Java (and surely in other languages too), you'd use a asterisk (*) to denote that but I understand Dart doesn't have the import syntax to achieve something like that. Though, I don't think that'd work with calling static methods, like BorderRadius.circular() or EdgeInsets.all().

imo, keep the dot . in front of the shorthand, it's more readable

100% agree. For EdgeInsets, .all() is a lot more readable than all(), and its what's done in other languages with enums.

This being an opt-in feature with the in syntax doesn't sit right with me. I can sort of understand it when dealing with constructors but at the very least enum's shouldn't have to be opt-in. As Jacob said, package authors shouldn't care about how parameters are passed syntactically.

lukepighetti commented 1 month ago

Strongly recommend the leading dot syntax for this. It's a really nice way to indicate to the programmer that it's shorthand enum syntax instead of some other thing in scope.

As far as I'm concerned, this only needs to work when the type is explicit and an enum. Bonus points for working with named constructors / factories / static members that return the same type

enum MyEnum { foo, bar}

final MyEnum x = .foo; // success
final y = .foo; // syntax error

void fn(MyEnum x) => null;

main(){
  fn(.foo); // success
}
eernstg commented 1 month ago

@cedvdb wrote:

color: Colors.green should be green.

True, thanks! Fixed.

keep the dot . in front of the shorthand

I would be worried about that. New syntactic forms of expression is always an extremely delicate matter, because it makes every expression more likely to be syntactically ambiguous.

eernstg commented 1 month ago

@jakemac53 wrote:

users might want to be able to opt out

That should not be necessary: Anything that currently has a meaning will continue to have that meaning (because we're using the standard scope rules). So you'd just write what you would write today, and it would never trigger this mechanism.

tatumizer commented 1 month ago

If you are OK with the new syntax, then instead of in clause or with clause (which every other user will forget to add), we can target the root cause by allowing the syntax like

class Colors simulates Enum<Color> {
  static Color red = ...
  //etc
}
Reprevise commented 1 month ago

What would be the difference between defining a global method with the same signature as one defined in the class if we don't keep the leading .?

EdgeInsets all(double value) {
  // ...
}

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

foo(padding: all(16));
eernstg commented 1 month ago

@jakemac53 wrote:

what if we just had a more general feature to add static members into the top level scope?

This could add a lot of names to the top-level scope. It might be difficult to manage them and avoid name clashes. We could consider local imports, https://github.com/dart-lang/language/issues/267. That is definitely one way to provide direct access to a set of names in some other scope (it's got one vote at this time ...).

eernstg commented 1 month ago

@Reprevise wrote:

What would be the difference between defining a global method with the same signature as one defined in the class if we don't keep the leading .?

The main difference is that the top-level function would pollute the name space much more pervasively: Every occurrence of all would then resolve to a declaration (the top-level function that you mention, or some nested declaration that shadows it).

With the mechanism proposed here we would only be able to call all(...) when the particular formal parameter admits the transformation (for example, from all(...) to EdgeInsets.all(...)).

So, for example, this mechanism would allow many different constructors whose name is of the form *.all to coexist. With a top-level function you'd have to choose one of them.

tatumizer commented 1 month ago

I should be able to do the following : String status in 'on' | 'off' .

What is the type of the expression on | off ? Isn't it a kind of Enum? What if you want to declare a type with two values on and off ? What syntax will you use?

cedvdb commented 1 month ago

@tatumizer (removed my previous comment before you quoted me but)

Type 'on' | 'off'

Same as type Colors.green | Colors.red | ... which can be generated from static members of Colors by a macro or the language ( with an "in" keyword for example). How the subset is generated is a detail.

Enum shorthand syntax is a different feature imo, but the two seem to be conflated in the proposal.

bernaferrari commented 1 month ago

I personally don't like this proposal. It would be 5x easier to just convert enums to strings like TS and support "contain", with no scoping problem, and union being easier as a bonus. I think what most people want is "left" | "center" | "right" (right now as an enum, but if it were an union type you wouldn't need to remember the class name, so win-win scenario). Swift is nice beause you type "." and it suggests the available types. You don't need to remember anything, just the ".". Similarly, TypeScript is nice because you type " and it suggests the available types.

In your proposal you loose this super important aspect, there is no way to type "something" and ask for the analyzer to suggest the options.

jakemac53 commented 1 month ago

I agree that one advantage of having the . prefix is it gives a good thing for autocomplete to work off of. I had the exact same thought.

cedvdb commented 1 month ago

In your proposal you loose this super important aspect, there is no way to type "something" and ask for the analyzer to suggest the options.

Note that autocomplete works with for example "ctrl + space" too without having to type anything but it may propose more options than necessary without "."

tatumizer commented 1 month ago

@cedvdb : And now you have 2 different concepts formalizing the "fixed set of values": one is the (existing) enum, and another is ... What do you call the type of 'on' | 'off'? If it's not an enum, then what is it? A second concept, parallel to enum? But if it is an enum, then you have to somehow shoehorn it into an existing concept of enum.

In principle, with static interfaces, you can do something like this:

@SimulateEnum()
class Colors {
   static Color red = ...
   static Color blue = ...
   ...
}

and make SimulateEnum macro add implements static Enum<Color> and define all the remaining Enum methods. But we don't have a parametrized class Enum<T> today.

StarProxima commented 1 month ago

How is this proposal different from this one?

The existing design proposal seems more thoughtful. Also, I agree that having starting points can simplify typing with autocomplete, also it would help avoid name collisions.

bernaferrari commented 1 month ago

Note that autocomplete works with for example "ctrl + space" too without having to type anything but it may propose more options than necessary without "."

Nothing beats " or .. Still half the muscle than ctrl+space (which changes depending on OS, machine and keyboard). " and . are always consistent.

For me this is unbeatable:

image
tatumizer commented 1 month ago

@StarProxima: In the existing proposal, there's a restriction

We restrict to static members returning the same type as the type declaration they’re on

This doesn't allow Color values defined in the class Colors to be used with the context type Color.

StarProxima commented 1 month ago

@tatumizer

Is it really necessary to support the Colors class? In theory, we can get colors from many places, from our own class with static fields, from ThemeExtension, directly using the constructor...

It would be weird to support only the Colors class for the possibility of shorthand.

Supporting enum, static fields and constructors (static methods?) already covers most use cases and is fairly obvious without requiring changes to existing code to support shorthand for use.

I would vote for the existing design proposal to address this issue https://github.com/dart-lang/language/issues/357.

StarProxima commented 1 month ago

@eernstg

If we want to allow Colors class, perhaps we could add some annotation to support the use of Colors whereverColor is required (perhaps on the declaration of the Color class itself, rather than on each use)? I think that would be easier than introducing new syntax into the language.

cedvdb commented 1 month ago

@tatumizer

If it's not an enum, then what is it? A second concept, parallel to enum?

What is the theoritical type of Color color in Colors in the proposal anyway ? It's obviously not Color but a subset. In that way, String text in 'on' | 'off' is not different, as a subset of String (implementation details aside).

eernstg commented 1 month ago

@jakemac53 wrote:

the . prefix is it gives a good thing for autocomplete to work off of

That's a very strong argument in favor of having the leading .. I'm looking into the grammar in order to try to learn some more about the impact of allowing '.' <identifier> as a <primary>.

@StarProxima wrote about the proposal named 'Dart enum value shorthand' from @lrhn:

How is this proposal different from this one?

You could say that all these proposals (we've discussed a bunch of them, some more elaborate than others, over a period of several years) are concerned with the selective access to namespaces that are not otherwise available.

(I'll use 'namespace' to refer to the static declarations of a class, mixin, extension type, extension, etc, because it's just a mapping from names to values that we are using.)

In particular, it allows id or .id (or some other variation of that theme) to have the meaning E.id because we have a reason to look for the name id in the namespace associated with E.

We could do that directly by opening the scope (that's a local import, #267). We could do it by putting a . in front of the identifier (that's https://github.com/dart-lang/language/blob/main/working/3616%20-%20enum%20value%20shorthand/proposal-lrhn.md). We could do it by declaring that specific locations will provide the namespace (such as this proposal).

(By the way, I don't think there would be anything wrong with a proposal which is similar to this one, but a '.' in front of an identifier would be used to trigger the mechanism. That should work just fine together with the in E mechanism. It could probably be optional, and it could be used to insist that we should use the extra namespace even in the case where the given name is already declared somewhere in an enclosing scope. I'm just worried about the syntactic cost ...)

So that was the trigger mechanism: How do we gain access to that extra namespace?

The next question is which namespace we're talking about. Several proposals rely on the context type, including 'Dart enum value shorthand'. In other words, for an actual argument to a function/method/constructor invocation, we look up the parameter type and use that in order to select the namespace. Concretely, if the parameter type is Color then we find the class denoted by the identifier Color (that is, the one which is in scope at the location where the formal parameter is declared), and then we try to find something named id in that namespace.

In contrast, this proposal relies on a user-specified namespace. For example, for a parameter of type Color we can make the choice to specify a namespace like the class Colors, which will then be used to look up id.

One reason why I've chosen to use an explicitly specified namespace is that this allows us to have specific choices for a specific context. For example, if we want to provide symbolic names for a set of magic numbers then we can do this:

extension MagicNumbers on Never { // An extension on `Never`: This is nothing but a namespace
  static const theBestNumber = 42;
  static const aBigNumber = 1000000;
  static const aNegativeNumber = -273;
}

void f(int number in MagicNumbers) {...}

void main() {
  f(theBestNumber); // Means `f(42)`.
  f(14); // Also OK.

  int i = 0;
  f(i); // Also OK.
}

If we were to insist that the chosen namespace for every int must be the int class then we wouldn't be able to have specific "application domains" for the same type, we'd have to stick to very specific sets of values like a specific enum, which would then be used in exactly the same way whenever it's used at all.

It is quite important for this proposal that we can have something like static extensions to provide extensibility.

static extension on MagicNumbers {
  static const aRandomNumber = 87;
}

void main() {
  f(aRandomNumber + theBestNumber);
}

Surely, static extensions would also be useful in collaboration with 'Dart enum value shorthand'.

Another thing to note is that we can offer values of more than one type in the given namespace.

extension MyNumbers on Never {
  static const int numberOne = 1;
  static const double numberHalf = 0.5;
}

void f<X extends num>(X x in MyNumbers) {}

void main() {
  f(numberOne); // Means `f<int>(1)`.
  f(numberHalf); // Means `f<double>(0.5)`.
}

This illustrates that it is (1) possible to provide values of different types in the same namespace (the call site chooses a name like numberOne and hence the caller is in control with respect to the chosen value and type); and (2) the chosen type could influence type inference.

Another way to use the ability to have multiple types (rather than relying strictly on the context type) is that this allows us to use subtypes of the context type. For example, both numberOne and numberHalf can be passed as arguments to a function whose parameter is num n in MyNumbers.

Finally, the explicitly declared namespace allows for combinations of namespaces num n in MyNumbers, MagicNumbers. Presumably, it would be OK for those namespaces to have conflicts, which would be resolved in a standardized manner (for example, if name has two different declarations then it's the last namespace that wins). This is again not so easy to do if we rely on the context type as the only and final selector of the namespace.

eernstg commented 1 month ago

@cedvdb wrote:

What is the theoritical type of Color color in Colors in the proposal anyway?

As the title may hint, this proposal is about providing some default values from a specified namespace. The type of color is Color, and any instance of type Color can be passed. But we expect that certain values of that type are particularly useful, and hence we'd like to provide support for denoting them in a concise manner. The role played by said namespace is to contain declarations of those "extra useful" values.

As always, we may also find a static method m in said namespace, and we may have an invocation like m([1, 2]) which turns out to mean MyNameSpace.m([1, 2]). This means that we have a set of "extra useful" functions, but we can also choose to pass any "normal" expression whose type fits the given parameter.

Also, constructs like switch (e) in E { ... } have yet another variant of similar properties.

cedvdb commented 1 month ago

@eernstg Snap, my mind immediately jumped on a feature I wished for. So it would not be a compilation error to pass a Color that is not in Colors despite the fact that I'm declaring Color color in Colors. It's just an hint or "default values". In retrospect that makes sense for the Colors case...

Although "in" is a loaded term, especially in the DB world, ence the confusion, well that and the fact that I read the proposal diagonally.

tatumizer commented 1 month ago

@StarProxima : upon reflection, I see that the Colors class is an outlier. Had these "named colors" been defined in the class Color directly, they would comply with the restrictions of the proposal you cited - problem solved. Why aren't they there? My guess is that Color is a general-purpose class defined in dart:ui, but Colors comes from flutter:material and defines color names according to the spec of Material. (Other standards may have different color names/values). The simplest way to make these colors available for shortcuts is to define

class MaterialColor extends Color {
  const red = MaterialColor(0xFFFF0000); // just copy/paste the definitions from Colors using a script.
  const blue = MaterialColor(...);
  ...
  const MaterialColor(int value) : super(value) {}
}

Then the restriction is satisfied. Another (better?) option could be: do nothing and leave Colors alone :-)

Abion47 commented 1 month ago

The opt-in requirement is a massive turn off. Any new feature like this that requires opt-in is a feature that may as well not exist as the vast majority o. It's also a massive problem for any library where the function is defined in a separate package than the type:

// Canvas class from dart:ui
class Canvas {
  void drawColor(Color color, BlendMode blendMode);
}
// Colors, MaterialColor from flutter/material
class Colors {
  static const red = MaterialColor(....);
  static const orange = MaterialColor(....);
  static const yellow = MaterialColor(....);
  static const green = MaterialColor(....);
  static const blue = MaterialColor(....);
  static const purple = MaterialColor(....);
}

class MaterialColor extends ColorSwatch<int> {}

There is no way for Canvas.drawColor to say it supports from Colors without adding a tight coupling from the dart:ui library to the flutter/material library and there is no extension syntax that would allow the flutter/library to patch the function with support. And even if a proposal added such syntax syntax, it would require a significant amount of fragile boilerplate and code duplication to achieve this functionality.

Also, what if someone had their own CustomColors list that they wanted to be able to use? This proposal doesn't allow for specifying more than one source type after the in and any solution I can think of would add sources of ambiguity. And even ignoring that, the developer would have to manually extend every function that supports Colors to add support for CustomColors which would get problematically tedious very fast.

I'm sorry, but this proposal is a complete non-starter IMO. It solves very few aspects of the original issue while adding a fair amount of entirely new issues, and every way I look at it, it adds far more work than it saves.

rrousselGit commented 1 month ago

I think the danger associated with a very broad mechanism that would enable red to be transformed into Colors.red in a very large number of locations (like, "in every location where the context type is Color") is more serious than the convenience of being able to cover cases like fn(red) can justify. This is particularly true because the type argument which is passed to fn is probably going to be inferred, not explicit.

I personally don't want fn(red), but instead fn(.red), where that .red is replaced with Color.red (not Colors.red. This would then have to be combined with static extensions, to have Colors values be available on Color).

The difference is pretty big. It is much simpler to understand what's happening and does not conflict with other possible top-level red or this.red variables.

As such, I disagree that a broad mechanism to do this is actually dangerous.

eernstg commented 1 month ago

Wow, absolutely everybody hates this proposal! 😁

Well, I've made some adjustments. First, I've experimented with the grammar and concluded that it isn't so dangerous after all to allow '.' <identifier> as a primary. So that's now included. It is used to force the lookups in the default scope (that is, it forces the given identifier to be looked up in the declared default scopes, disabling all the local scope rules).

Next, it seems likely that this kind of lookup will always be useful when the associated type is an enumerated type. Hence, when E myParameter is encountered and E is an enumerated type, it is implicitly changed to E myParameter in E. Similarly for switches whose scrutinee has a static type which is an enumerated type.

enum MainAxisAlignment {
  start,
  end,
  center,
  spaceBetween,
  spaceAround,
  spaceEvenly;
}

void f(MainAxisAlignment alignment) {
  switch (alignment) {
    case .start: ...;
    ...
    case .spaceEvenly: ...;
  }
}

Finally, a number of detailed issues in the semantics have been made more precise. In particular, actual type arguments can not be passed explicitly to a constructor invocation because those type arguments are passed to the class part of the constructor name, not the "last name". We might be able to find a good syntax for that, but for now we just have to write the constructor invocation like today, without any abbreviations.

@cedvdb wrote:

Snap, my mind immediately jumped on a feature I wished for. So it would not be a compilation error to pass a Color that is not in Colors despite the fact that I'm declaring Color color in Colors. It's just an hint or "default values". In retrospect that makes sense for the Colors case...

Right, it's a "default scope". In other words, if the name isn't in scope locally then we can go to the default scope and find it. When the prefix period is used, we must find the name in the default scope.

About the word in: It is indeed rather vague. We could use default in rather than just in. On the other hand, requests for brevity are more common than requests for long, self-explanatory sequences of keywords. ;-)

@tatumizer wrote:

Had these "named colors" been defined in the class Color directly, they would comply with the restrictions of the proposal you cited - problem solved. Why aren't they there? My guess is that Color is a general-purpose class defined in dart:ui, but Colors comes from flutter:material and defines color names according to the spec of Material. (Other standards may have different color names/values).

Exactly, so why don't we just embrace the notion that we can have a widely used type (like Color), and we may want to provide easy (abbreviated) access to a specific subset of instances of that type, and then we may (obviously!) want to use more than one such subset.

@Abion47 wrote:

The opt-in requirement is a massive turn off.

I guess this is a reference to the fact that the original proposal required an in E clause on the given parameter in order to do anything at all?

This has been adjusted such that enumerated types always get an in Something: We can declare it explicitly if we want something special, otherwise we just use the enumerated type itself as the default scope. So you shouldn't be turned off that massively with the new updates. ;-)

There is no way for Canvas.drawColor to say it supports from Colors without adding a tight coupling from the dart:ui library to the flutter/material library and there is no extension syntax that would allow the flutter/library to patch the function with support. And even if a proposal added such syntax syntax, it would require a significant amount of fragile boilerplate and code duplication to achieve this functionality.

Those are well-taken points!

It is not a trivial exercise to allow for extensibility in any mechanism, and there are so many examples of research whose goal is to support one or the other kind of extensibility. Dependency management comes up frequently. We probably don't have any proposals that are perfect in this respect.

However, I believe there is a way forward:

Assume that Dart adds support for static extensions (#723 has 742 upvotes today).

We could then provide the colors in the class Colors as members of a static extension. As far as I can see, nobody outside colors.dart can use the class Colors for any other purpose than looking up static members.

We could then consider changing the material Colors to be a static extension. It might be a static extension of Color in dart:ui, but it could also be a static extension of a new declaration named Colors in dart:ui (if we're afraid that Color would get too crowded).

So let's assume that we introduce that new declaration named Colors in dart:ui, and then change the material Colors to a static extension that populates the dart:ui Colors:

// dart:ui

class Canvas {
  void drawColor(Color color in Colors, BlendMode blendMode);
}

extension Colors on Never {
  // A home for a bunch of colors, added via
  // `static extension on Colors {}`.
  // This could be empty, it would rely on some other
  // library to populate this namespace with some colors..
}
// Colors, MaterialColor from flutter/material

export 'dart:ui' show Colors; // To avoid breakage, if needed.

static extension MaterialColors on Colors {
  static const red = MaterialColor(....);
  static const orange = MaterialColor(....);
  static const yellow = MaterialColor(....);
  static const green = MaterialColor(....);
  static const blue = MaterialColor(....);
  static const purple = MaterialColor(....);
}

class MaterialColor extends ColorSwatch<int> {}

With this approach there is no dependency from dart:ui to material.dart, we just need a namespace (Colors in dart:ui) that we can populate from anywhere using static extensions.

tatumizer commented 1 month ago

@eernstg : please look at this comment and below. This idea (inspired by swift) allows us to achieve essentially the same result with a much simpler design. There's no need to do much about the existing declarations; the rules are simple: given a context type Foo, you are allowed to write .method for any static (this includes constructors) method defined in Foo (the latter is not necessarily being an enum type). And for Colors, you have to just formally re-classify the existing Colors class as a static extension.

It's not that your design is bad or something. It's just more complicated than the alternative, so the cost/benefit ratio is higher.

It's much easier to explain to the user that when the context type is Foo, the expression starting with .id is equivalent to Foo.id - the rest follows from here.

Supporting the chain is necessary, otherwise, you can say color: Colors.red.withOpacity(0.5), but won't be allowed to say color: .red.withOpacity(0.5)

bernaferrari commented 1 month ago

I find it surprising how many people want Colors, specially since Flutter uses Material 2014 palette which is super bad and even Google has been recommending not using it for years. Tailwind has a much better palette, and has changed colors 4 times on the past 4 years. Swift has also changed colors a few times, and now has like 4 variations for each color (supporting dark theme, high contrast, or both).

I would say, focus on enums first and let colors for later. My biggest fear is slowness, specially with Material Icons, where typing "." might or might not slow down things while it searches for all possible icon choices.

The switch example is good, I like it. But you are almost into unions territory. My dream is still this being possible one day:

void f("start"  | "center" | "end" alignment) {
  switch (alignment) {
    case "start" : ...;
    case "center" : ...;
    case "end" : ...;
  }
}
tatumizer commented 1 month ago

I would say, focus on enums first and let colors for later.

Enums alone are not enough. In Flutter, there are lots of "enum-like" classes (Alignment is a representative example). They cannot be made into enums because they implement a general concept, but provide a number of frequently used predefined constants, which is what everybody uses most of the time. Supporting only enums will leave the users wondering why in 50% of cases they can write .id, and in the other 50% cannot. The whole motivation for this enhancement is to do something about cases like alignment: Alignment.bottomCenter, which is clearly redundant.

The reason for fixation on Colors is that 1) it's not the only class exhibiting the problem (e.g. Icons is similar), 2) if the enhancement cannot cope with these classes, then again, the users will be wondering... (see above)

My biggest fear is slowness, specially with Material Icons, where typing "." might or might not slow down things while it searches for all possible icon choices.

The speed will be the same as it was before; the dot will be just a shortcut to Icons., and IDE will have to do the same amount of work (they most likely cache the results anyway).

Abion47 commented 1 month ago

@bernaferrari

I find it surprising how many people want Colors, specially since Flutter uses Material 2014 palette which is super bad and even Google has been recommending not using it for years. Tailwind has a much better palette, and has changed colors 4 times on the past 4 years. Swift has also changed colors a few times, and now has like 4 variations for each color (supporting dark theme, high contrast, or both).

I would say, focus on enums first and let colors for later. My biggest fear is slowness, specially with Material Icons, where typing "." might or might not slow down things while it searches for all possible icon choices.

The switch example is good, I like it. But you are almost into unions territory. My dream is still this being possible one day:

void f("start"  | "center" | "end" alignment) {
  switch (alignment) {
    case "start" : ...;
    case "center" : ...;
    case "end" : ...;
  }
}

It's not that people are specifically wanting Colors. It's simply a common example of the way the Flutter SDK is designed, with static members of classes that may or may not be related to the target class. Another example is Padding which takes a property of type EdgeInsetsGeometry for the padding parameter but the most common use case is to call a factory constructor of the derived EdgeInsets class. A third is Icons, which is a container for many static instances of IconData but is itself unrelated to the IconData class. A proposal for the static dot syntax has to account for all of these use cases, because if it doesn't, the feature will be either unusable or inconsistent within Flutter, at which point, why even bother?

(And you may point out that this wouldn't have been a problem had Flutter been designed differently, but that complaint is entirely irrelevant and unhelpful. It is what it is, and now we simply have to deal with it.)

Also, type unions are an entirely different (and extremely complex) beast that have nothing to do with this feature, and it's not like the features are mutually exclusive.

bernaferrari commented 1 month ago

You are right, I forgot about padding

cedvdb commented 1 month ago

Different non overlapping defaults could be supported something default in X | Y . Just a thought, not sure if useful

rrousselGit commented 1 month ago

Unions don't really solve the underlying problem.

I personally struggle to see why we don't implement "using the .identifier shorthand in a context where the parameter is of type T is strictly equivalent to T.identifier". This has been suggested many times, but we seem to be talking around the subject. What's the issue with it and why isn't it considered more? Assuming that we'd support Colors and such through static extensions, so that instead of Colors.magenta we'd do Color.magenta, which could use the .magenta shortcut.

The rule is simple, and it's fairly flexible. We can support things like:

Padding(padding: .all(16));
Padding(padding: .only(left: 10, top: 5));
eernstg commented 1 month ago

@tatumizer wrote:

This idea (inspired by swift) allows us to achieve essentially the same result with a much simpler design. There's no need to do much about the existing declarations; the rules are simple: given a context type Foo, you are allowed to write .method for any static (this includes constructors) method defined in Foo (the latter is not necessarily being an enum type). And for Colors, you have to just formally re-classify the existing Colors class as a static extension.

I think the complete reliance on the context type works very nicely for enumerated types. That's also the reason why I made it the default for enumerated types: You basically always want to select a value from the complete set of instances of that type.

However, even for an enumerated type we could have situations where we want something different:

enum E { one, two, three, four }

abstract final class EvenE {
  static const two = E.two;
  static const four = E.four;
}

class C {
  final E e;
  C.anythingGoes(E this.e);  // Use the default `in E`.
  C.onlyEven(E this.e in EvenE); // Offer only the "even" values.
}

void main() {
  C.anythingGoes(three); // OK.
  C.onlyEven(two); // OK.
  C.onlyEven(one); // Compile-time error.
}

The need to avoid putting every possible named value of a given type into the same bucket is of course much more acute with widely used types like String or int, as I mentioned along with the MagicNumbers example earlier.

The point is that the context type as such is too broad to be the sole identification of a relevant set of named values—there will be formal parameters with the same type that do not have the same set of relevant named values, and it's going to burden us with some polluted name spaces if we insist that they must always go into the same bucket.

Colors is actually a good example: They did not put all the material colors into Color. I don't know exactly how those discussions went, but it might very well be because that would canonicalize this particular set of colors. There will be a Material 4 whose colors could be slightly different, and it's probably not going to look very good if you mix and match colors from different palettes.

Supporting the chain is necessary

As an experiment, I included support for selectors and cascades. See the end of this comment, too.

@bernaferrari wrote:

I find it surprising how many people want Colors, specially since Flutter uses Material 2014 palette which is super bad and even Google has been recommending not using it for years. Tailwind has a much better palette, and has changed colors 4 times on the past 4 years

To me this sounds exactly like we'd want support for choosing a set of relevant default values, not forcing everybody to use the same union-of-all namespace.

@cedvdb wrote:

Different non overlapping defaults could be supported something default in X | Y

I think this would be the same thing as in X, Y, which is supported in this proposal (assuming that X and Y denote classes or other entities that are capable of declaring static members).

@rrousselGit wrote:

I personally struggle to see why we don't implement "using the .identifier shorthand in a context where the parameter is of type T is strictly equivalent to T.identifier".

In this proposal, that is indeed the default treatment of any parameter or switch scrutinee whose type is an enum.

We could extend the semantics such that .identifier (and derived forms like .identifier<num>(14).bar[0]) would have the meaning T.identifier (respectively T.identifier<num>(14).bar[0]), but I don't think it's going to generalize very gracefully, in two respects:

For example, who says that the static type of T.identifier<num>(14).bar[0] is assignable to T? Alternatively, are we supposed to track down a type T1 such that T1.identifier<num>(14).bar[0] has a type which is assignable to T?

rrousselGit commented 1 month ago

For example, who says that the static type of T.identifier(14).bar[0] is assignable to T? Alternatively, are we supposed to track down a type T1 such that T1.identifier(14).bar[0] has a type which is assignable to T?

I don't think that's a concern. We'd get an assignment error then. If T.foo isn't assignable to T, fn(.foo) would give the exact same error as fn(T.foo).

The syntax wouldn't mean "sugar for possible values of type T", but "When typing .foo, T.foo was inferred". Similar to how we can type final foo = 42 and it gets inferred as final int foo = 42. We know that the initializer has a context type of type int, so we infer int foo

The context is not a type, it is a type schema (which is a type except that it may have occurrences of the unknown type, often written as , which is subject to type inference). What is the semantics if we try to look up .someIdentifier in ?

If there's no context type, then it should be a compilation error to use .foo. The type could not be inferred, so we emit an error.

What is the semantics if the context type is a type variable?

We can use .identifier only on known type variables. So we can't do:

void fn<T>(T value) {
  fn<T>(.identifier); // Makes no sense, we cannot infer "T."
}

But we can do:

void fn<T>(T value) {
  fn<int>(.parse('42')); // Valid. We know that `T` is of type `int`. So we infer `.parse` as `int.parse`.
}
rrousselGit commented 1 month ago

Using analyzer terms, I'd implement this by relying on parameterElement and staticElement. If they are null, InvalidType or TypeParameterType, then using the .identifier syntax results in a compilation error (due to the lack of contextual information).

Meaning we could do:

final int foo = .parse('foo');

int fn() => switch (.parse('foo')) {
    ...
}

Color c;
switch (c) {
  case .red: ...
}

This includes more complex expressions too. Like:

final int foo = .tryParse('foo') ?? 42;
// Equivalent to:
final int foo = int.tryParse('foo') ?? 42; 

Because int the staticType of this expression.

It is the users' responsibility to ensure that this expression will indeed return a value assignable to the expected type.

tatumizer commented 1 month ago

@eernstg: in other words, you are introducing constrained subtypes without calling them this way. This opens Pandora's box. There are many cases where you'd want them, e.g. someone might be interested in small integers in the range 1..100. Ada goes all the way in this direction: see this section, skip to 3.3.2.

I don't question the wisdom of introducing such types. But here, it all looks like shooting sparrows from cannons. and missing. A small issue of allowing .id syntax becomes dependent on a very large issue, which is (nonetheless) unlikely to be able to address the problem at hand.

Suppose the user types color: Color. and presses CTRL-SPACE. What suggestion is expected here? Probably, all static methods and constants and constructors of Color, right? Even if Color class itself included a constrained subtype of possible colors that provided 100 named colors out of the box, this is not the reason for not showing a suggestion for the constructor Color.fromARGB(int a, int r, int g, int b).

It's much worse with flutter's Colors. They list just a number of colors preselected for Material scheme. But you can't constrain the color: parameter of any widget to these colors (something that in clause suggests) Someone may hate these colors and love CupertinoColors instead. But the widget is so constrained that it cannot understand CupertinoColors. The number of color palletes is practically unlimited - anyone can define their own pallette.

There's no way to handle this issue exhaustively to everyone's satisfaction. What I propose is a modest trick: re-formulating Colors into static extension, and then .red will automatically mean at least something. If you don't like that, you can later change .red to CupertinoColors.systemRed. Or maybe Flutter can make CupertinoColors a static extension. We can't cover all potential options for color: attribute anyway - e.g. ColorSwatch has a constructor, but this constructor will hardly be found by IDE. But the users will hopefully understand the limitations.

eernstg commented 1 month ago

For example, who says that the static type of T.identifier(14).bar[0] is assignable to T?

... If T.foo isn't assignable to T, fn(.foo) would give the exact same error as fn(T.foo).

The point I was making was that it seems silly to support chains of method and getter invocations in general if the resulting expression is unlikely to be type correct in the given context.

Assume that we encounter .identifier(14).bar[0] at a location where the context is the plain class type T (just to say that there's nothing hard about the context type schema itself in this case). The identifier could be the name of a static member of T, but it seems quite unlikely to me that T.identifier(14).bar[0] has a type which is assignable to T. So why would we transform it into T.identifier(14).bar[0] in all cases, if that's just a type error in most cases?

It seems like it would be a much more useful mechanism if we could find an S such that S.identifier(14).bar[0] has a type which is assignable to T. However, that's not so easy because we would need to "search all types" to find S.

The Dart style guide has a rule that says avoid returning this, and that basically makes it less likely that e.foo().bar[7].baz has the same type as e.

I think cascades are much more promising in this respect: We can definitely transform .identifier(14)..bar[0] into T.identifier(14)..bar[0] when T.identifier is a constructor of T or a static method whose return type is T.

I don't know. I don't think transforming .identifier(14).bar[0] into T.identifier(14).bar[0] just because the context type is T appears to be a beautiful or promising language mechanism.

Perhaps it's can work, and developers would just need to navigate the typing properties of those chains with care. But it does sound like a recipe for surprises and disappointments to me.

Similar to how we can type final foo = 42 and it gets inferred as final int foo = 42.

That's very different because in that case we're inferring a type int which is guaranteed to preserve the type correctness.

Transforming .identifier(14).bar[0] into T.identifier(14).bar[0] because the context type is T seems more like inferring final int foo = 42.isEven; because 42 has static type int. ;-) We're by design ignoring the type of the expression as a whole and just latching onto the leading token (.identifier respectively 42).

But we can do:

void fn<T>(T value) {
  fn<int>(.parse('42')); // Valid. We know that `T` is of type `int`. So we infer `.parse` as `int.parse`.
}

(I assume that the invocation of fn could be anywhere, it's just convenient to have it in the body of fn because we need to have a declaration as well as an invocation of fn. So it doesn't matter that it's an infinite loop.)

That's a good point!

That is a case where the context type is int, and there is no reasonable way to specify in the declaration of fn that the parameter uses a default scope. So that is definitely a case where it could be argued that it is convenient to support the transformation from .parse to int.parse based on the context type alone.

We could allow the form with the leading period to use the context type like this (that is, it would be allowed in all locations where there is a context type), if nothing else is specified. Of course, C<int>? would yield C if that's a class that declares any static members or constructors.

That should cover the following:

This includes more complex expressions too. Like:

final int foo = .tryParse('foo') ?? 42;
// Equivalent to:
final int foo = int.tryParse('foo') ?? 42; 

The context type for .tryParse('foo') is int?, so we'd consider constructors/static members of int.

@tatumizer wrote:

you are introducing constrained subtypes

Nono, there is no subtype relationship between E and EvenE, I'm just illustrating that if we don't recommend using every possible value from an enumerated type as an argument for a specific parameter then we can express that, too.

Suppose the user types color: Color. and presses CTRL-SPACE. What suggestion is expected here?

As of today, Color does not contain a bunch of constant variables holding specific colors. It's probably a good idea to keep it that way. For example, I don't think it's going to be pleasant if that particular situation would cause all named colors in the entire imported world to pop up.

So the completion would yield just those static methods and constructors of Color that you'd get today.

This is a pretty good reason why we don't want to limit ourselves to an unconditional 100% reliance on the context type. We want to make it possible for other namespaces than Color to provide named colors.

It's much worse with flutter's Colors. They list just a number of colors preselected for Material scheme. But you can't constrain the color: parameter of any widget to these colors (something that in clause suggests) Someone may hate these colors and love CupertinoColors instead. But the widget is so constrained that it cannot understand CupertinoColors. The number of color palletes is practically unlimited - anyone can define their own pallette.

It is possible for dart:ui to support a customizable set of colors (as mentioned here):

// --- 'dart:ui'.

class Canvas {
  void drawColor(Color color in Colors, BlendMode blendMode);
}

abstract final class Colors {
  // A home for a bunch of colors, added by other libraries via
  // `static extension on Colors {...}`.
}

// --- 'material.dart'.

static extension MaterialColors on Colors {
  static const red = MaterialColor(....);
  static const orange = MaterialColor(....);
  static const yellow = MaterialColor(....);
  static const green = MaterialColor(....);
  static const blue = MaterialColor(....);
  static const purple = MaterialColor(....);
}

// --- 'cupertino.dart'

static extension CupertinoColors on Colors {
  ...
}

If your program imports 'material.dart' then the material colors will be available when passing arguments to a Canvas, and if you import 'cupertino.dart' then the cupertino colors will be available, and you can even have both (unless that creates a lot of other conflicts ;-).

There's no limit on the number of palettes you can have with this approach, and also no limit on the number of separate contributions you can add to ui.Colors, if needed (you will just get a compile-time error if you've added two colors to the same namespace with the same name and then try to use that name).

What I propose is a modest trick: re-formulating Colors into static extension

Yes, that's exactly the approach that I had in mind as well. Of course, the details may be tricky to get right, and it's definitely not acceptable to cause widespread breakage.

tatumizer commented 1 month ago

@eernstg:

class Canvas {
 void drawColor(Color color in Colors, BlendMode blendMode);
}

Maybe I misunderstand the meaning of this declaration, but to me, it reads like: "color parameter should be one of the colors defined in Colors subclasses and extensions on Colors, and only in these places". The meaning of in, by its nature, is quite restrictive. When we say for (var i in [0, 1]) we really mean "all those values and nothing else". But color parameter should be able to accept, among other things, the values like Color.fromARGB(...) and, generally, any variables of type Color. How do you enable them? Static extension cannot declare constructors AFAIK. Either I don't understand the meaning of "in", or the definition is too restrictive.

Another point is that any method with color parameter is now supposed to add in Colors in their definition. There can be uncountable methods in flutter that accept color, and now what? All of them have to modify their signature? Maybe we can just say somehow once: whenever the parameter is of type Color, enable the logic of substituting the shorthands defined in Colors (and, by implication, extensions on Colors) with a shorter notation .id ?

(In general, it feels like you are moving in a right direction, but I can't understand the details, sorry)

Edit: how about

abstract final class Colors providing_shortcuts_for Color { // better wording is needed
  // A home for a bunch of colors, added by other libraries via
  // `static extension on Colors {...}`.
}

The idea of aggregating the shortcuts, and only the shortcuts, leads to a cleaner solution, but in??? The above definition contains enough information to make in unnecessary. It automatically applies to every parameter of type Color. There can be several extensions providing shortcuts for Color (MaterialColors, CupertinoColors etc), and they can be imported selectively

cedvdb commented 1 month ago

@tatumizer I misunderstood also at first. It reads as default can be found in, which I would abbreviate to default in personally, certainly not just in

tatumizer commented 1 month ago

@cedvdb: but why? One extra clause in the definition of Colors class (see above) is enough to trigger the logic of substitution for every parameter and variable of type Color!