Skip to content

Parameter default scopes #3834

Open
Open
@eernstg

Description

@eernstg

In response to #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:

  • Assume that P is a parameter declaration whose declared type is an enumerated type E. If P does not have a default scope clause then in E is induced implicitly.
  • Assume that S is a switch expression or statement that does not have a default scope clauses, and whose scrutinee has a static type E which is an enumerated type. In this case a default scope clause of the form in E is implicitly induced.
  • Finally, assume that an expression .id derived from '.' <identifier> is encountered at a location where the context type is of the form C, C?, C<...>, or C<...>?, where C is an identifier or a qualified identifier that denotes a class, mixin, mixin class, or an extension type. Assume that C declares a static member named id or a constructor named C.id. In that situation .id is replaced by C.id. As in the previously declared cases, this rule is also extended to the case where .id is followed by a chain of member invocations and/or a cascade.

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.

Enumerated types

An enumerated type is specified in terms of an equivalent class declaration.

With this proposal, each enumerated type E will have an abstract declaration of operator == of the following form:

  bool operator ==(Object other in E);

Assume that E is an enumerated type that declares the value v and e is an expression whose static type is E. An expression of the form e == .someName (or e != .someName) will then resolve as e == E.someName (respectively e != E.someName).

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

  • Version seven, Friday June 14: Remove support for bare identifiers, only .id is supported now. This was done because it is likely to be hard to spot that any given plain identifier is looked up in a default scope, rather than using the normal scope rules.
  • Version six, Monday June 3: Remove support for multiple default scopes. The syntax was ambiguous (thanks to @Abion47 for pointing out this ambiguity), and the expressive power is already covered rather well by using static extensions to populate a single default scope.
  • Version five, Friday May 31: Add a recommendation to have a warning when a context type which is used as a default scope is obtained by promotion or type inference.
  • Version four, Wednesday May 29: Add a catch-all rule that transforms .id to T.id when no other rule is applicable. Change the support for selector chains and cascades to a part of the proposal.
  • Version three, Tuesday May 28: Mention support for selector chains (.id.foo().bar[14].baz) and cascades as a possible extension.
  • Version two, Monday May 27: Include dot-identifier. General rewrite and clarification.
  • First version posted on Friday May 24.

Activity

added
featureProposed language feature that solves one or more problems
on May 24, 2024
changed the title [-]Parameter default scopes?[/-] [+]Parameter default scopes[/+] on May 24, 2024
eernstg

eernstg commented on May 24, 2024

@eernstg
MemberAuthor

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

rrousselGit commented on May 24, 2024

@rrousselGit

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

eernstg commented on May 24, 2024

@eernstg
MemberAuthor

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

cedvdb commented on May 24, 2024

@cedvdb

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

eernstg commented on May 24, 2024

@eernstg
MemberAuthor

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

cedvdb commented on May 24, 2024

@cedvdb

@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

jakemac53 commented on May 24, 2024

@jakemac53
Contributor

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

jakemac53 commented on May 24, 2024

@jakemac53
Contributor

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

Reprevise commented on May 24, 2024

@Reprevise

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

lukepighetti commented on May 24, 2024

@lukepighetti

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

eernstg commented on May 24, 2024

@eernstg
MemberAuthor

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

132 remaining items

Abion47

Abion47 commented on Jun 14, 2024

@Abion47

This is the last time I'm going to reply on this thread, as I feel like at this point I have said everything that needs to be said, and after this you either understand and will address my concerns or you don't and you won't. In either case, it's been made pretty clear that we aren't going to see eye to eye on this, so there isn't much more reason to arguing in circles ad infinitum.

Did you read the proposal? What is it that you want and that you can't express using this proposal? I'll try to find the answer to this question as I'm reading your comments, but a bit of help would be awesome!

What I would want is to be able to add multiple types to a parameter scope so I don't need to arbitrarily combine everything into a single utility type. Your suggestion of having a single type that a user can extend using a static extension is something that would be considered a dirty workaround, not an official solution, and in many scenarios, it begs the question of why the user wouldn't just use a static extension on the parameter type itself (or, better yet, just add the members to the type directly).

Also, what if a function doesn't define a type for a parameter scope at all?

// in package:my_company_common/authorization.dart
Future<SignInResult> signIntoUserPool(String pool, String username, String password) { ... }

Imagine this is a function in a company's internal generic common library. In an implementation that references it, they might have several pre-defined pools relevant to that application:

// in package:my_company_client_app/.../auth_service.dart

import 'package:my_company_common/authorization.dart';

abstract class ClientUserPools {
  static const workers = 'workers';
  static const teamLeads = 'team-leads';
  static const supervisors = 'supervisors';
}

void signInWorker(String username, String password) {
  final result = await signIntoUserPool(ClientUserPools.workers, username, password);
  ...
}

It would be nice if they could shorten ClientUserPools.workers to just .workers, but they can't because the parameter doesn't specify a type as a default scope for them to extend. Sure, they could add a dummy type in the common library themselves, but what if this function was in a code base that the implementation author has no control over? What if it's in a third-party package? There's only so much the user can do before they would be forced to either extend String itself or just accept the verbosity, all because the package author didn't have the foresight to provide a dummy type on that parameter.

Also, what about the username and password parameters? Should the package author include dummy type parameter scopes on those fields as well on the off-chance some downstream user would want to extend them? Well that's three dummy types just for this one function. How many other functions are there, and should all those functions' parameters also have dummy types of their own? How many parameters need dummy types need to be created before they can consider the level of support for this feature to be satisfactory to address any given user's needs?

This is why this feature being opt-in is such a big issue. Not only can users not take advantage of it unless a package author explicitly supports it, but it is entirely the package author's responsibility to make sure it is supported anywhere a user might want to take advantage of it. And as you can see, package authors adding this support quickly turns into a slippery slope of trying to anticipate any potential user's use cases.

This approach proactively adds support for members of Colors to the parameter which would otherwise only support Color

This doesn't make sense. The parameter whose name is color in the example has type Color, and any expression of type Color (or a subtype thereof) is a type correct actual argument for that parameter. Conversely, no expression whose static type isn't Color or a subtype thereof (or dynamic) is accepted as an actual argument.

I would've thought that in the context my comment was made, I was making it clear that "adds support" was referring to dot syntax support, not to what values are considered valid for the parameter itself.

Conversely, static extensions are a retroactive extension:

extension MaterialColorExt on Color {
  static final Color red = Colors.red;
  ...
}

This approach retroactively adds support for members of Colors to the Color type itself.

I'd very much like to have static extensions, and as you may know this proposal has used them from day one, based on the hope that we'll get them (somewhat soon, too).

So we don't have to choose one or the other, I'm arguing that they work well together.

Again, you missed the point of my comment. I'm not trying to argue that they are mutually exclusive features. I'm trying to explain the difference between "proactive extension" and "retroactive extension", and why the latter is preferable to the former in almost every regard.

I think your term 'retroactively' covers pretty much the same concept as when I say 'extensible'.

No, again, see above. When I use the terms "proactive" and "retroactive", they are both in the context of extending a type and they describe different methodologies for accomplishing it.

That is not the point.

I can have points, too. ;-)

You can have points all you want, but when you make points that argue against points that I never actually made, that's called strawmanning, and it's generally frowned upon.

if a package author wants to make use of this feature to support their MyColors class in a package that has dozens or even hundreds of functions that take a Color parameter?

You're right, that could take several hours. It's a one time investment, though.

I don't know about you, but I for one would hate with every fiber of my being a feature that required me to spend hours writing out boilerplate just to make use of it. Not to mention that if I ever wanted to deprecate MyColors in favor of MyMaterialColors or something, that would require once again going back through all that boilerplate to make the change (or risk breaking something by using a find-and-replace tool).

And that's just for one type - if I also wanted to make use of MyThemes or MyMathConstants, I would have to go through all of that all over again. What you don't seem to understand is that this isn't a one-time deal. A typical package could easily contain a dozen or more of these collection types, and something like Flutter could conceivably contain well over a hundred. A parameter default scope would have to be added for each and every one of them to each and every function in the entire code base that references a related parameter. I would very quickly just go back to using static extensions because this sheer amount of boilerplate just isn't worth whatever perceived namespace pollution concerns I may or may not have.

If I understand this correctly, your idea is that there is some dart:ui.Colors class that is added to every function that takes a Color via Color color in ui.Colors. That class is initially empty with the intention that users will be able to add to it themselves using static extensions which would make them available everywhere.

Exactly. This is one possible approach. It does involve some machinery (and some work, initially), but it allows clients to populate that empty namespace in any way they wish.

It also adds potentially dozens of classes that literally do nothing to the global namespace for no reason whatsoever from the perspective of the vast majority of users who will never make use of them. Why are you not concerned with that form of namespace pollution but believe the concept of using a static extension to make Colors.red be generally accessible via Color.red is such a big deal?

A class in the official SDK that intentionally does nothing and is designed to be statically extended strikes me as an antipattern.

It's a hook, that is, a mechanism that allows clients to add functionality to something, e.g., a big framework.

You call it a hook, I call it a crutch for a poorly implemented mechanism.

Another example of a hook is a virtual method (in Dart: every class/mixin/enum instance method is virtual, but extension methods are not).

Not entirely accurate since Dart implemented final classes, but I digress.

The Template Method design pattern is an example where it is very clearly used as a hook.

Is that an antipattern?

This is a very apples-to-oranges comparison. This is like saying that because recursion is better than integration in one scenario, it is better in all scenarios.

Templates and hooks have their uses, but there are also plenty of use cases where they are the wrong thing to use because they address the wrong problems of the system or because they make things more complicated than they need to be. You wouldn't use an instance of a class with a virtual callback to pass the result value of a simple synchronous operation - you just return the result to the function caller.

Likewise, your solution of a "hook" type adds multiple layers of complexity to a scenario where a much simpler solution exists, and the sole benefit of doing so is to avoid having to see a handful of identifiers on a type's namespace. The subjective benefits are far outweighed by the objective downsides, and that is the definition of an antipattern.

the more you suggest shoring those issues with static extensions

As I mentioned, static extensions have been part of this proposal from day one. Extensibility is important!

I don't know how to make this any clearer. The point isn't that this feature and static extensions are mutually exclusive proposals. The point is that this feature has little reason to exist when A) static extensions do 99% of the same job but better, and B) the 1% that is left is both highly limited in usable situations and highly subjective in its beneficial nature. Saying static extensions are part of this proposal does nothing to address that point.


And with that, I officially rest my case on this matter. As a form of a parting summary, here are the primary issues your proposal has in no particular order that you have yet to adequately address:

  • It utilizes a syntax that is confusing and unintuitive in virtually every aspect of its design.
  • It introduces mandatory non-automatable boilerplate that results in potentially fragile code.
  • It's not user-extensible without relying on both a separate language proposal and a convoluted mess of "hook" types.
  • It forces the user of a function to be entirely dependent on the author of the function to explicitly add support for it, without which the user can't take advantage of it at all.
  • Specific to the hook types, it ironically results in an even worse namespace pollution than what the feature proposal claims to avoid.
  • It is almost entirely superseded by the very proposal it depends on for extensibility with the remainder of use cases being too few, too narrow, and too subjective to justify the implementation cost and added complexity.

Address the issues, argue them as incorrect, disregard them as unimportant, do what you will with them. It no longer concerns me.

added
dot-shorthandsIssues related to the dot shorthands feature.
brevityA feature whose purpose is to enable concise syntax, typically expressible already in a longer form
and removed on Nov 18, 2024
mmcdon20

mmcdon20 commented on May 1, 2025

@mmcdon20

I have been trying out the dot-shorthands experiment flag.

I do think that there are gaps where default scopes would be useful, but I don't think it will cover all of the relevant cases.

consider the following program:

void main() {
  if (DateTime.now() case DateTime(weekday: DateTime.saturday || DateTime.sunday)) {
    print('today is a weekend');
  } else {
    print('today is a weekday');
  }
}

Here we would ideally like to shorten the code to the following:

void main() {
  if (DateTime.now() case DateTime(weekday: .saturday || .sunday)) {
    print('today is a weekend');
  } else {
    print('today is a weekday');
  }
}

The problem here is that weekday is a getter external int get weekday;, so unless default scopes also applies to return types ie external int in DateTime get weekday; then it does not apply to the situation.

Perhaps one solution would be for the standard library to wrap the getter in an extension type which groups the values into a single namespace.

extension type const Weekday(int _) implements int {
  static const Weekday monday = Weekday(1);
  static const Weekday tuesday = Weekday(2);
  static const Weekday wednesday = Weekday(3);
  static const Weekday thursday = Weekday(4);
  static const Weekday friday = Weekday(5);
  static const Weekday saturday = Weekday(6);
  static const Weekday sunday = Weekday(7);
}

class DateTime implements Comparable<DateTime> {
  ...
  external Weekday get weekday;
  ...
}

Edit: Some additional thoughts on using extension type for parameter scoping.

Additionally, an extension type could be used to control the dot shorthand namespace for parameters, but the problem then is that if you don't want one of the predefined values you have to wrap the value in your extension constructor.

// assuming Weekday defined above

bool isWeekend(Weekday day) => day == .saturday || day == .sunday;

void main() {
  print(isWeekend(.saturday));
  print(isWeekend(Weekday(3)); // ideally we would like to do: isWeekend(3)
}

If extension type had support for an implicit constructor it would be like having a parameter default scope.

// with implicit constructor
extension type implicit const Weekday(int _) implements int { ... }

void main() {
  print(isWeekend(.saturday));
  print(isWeekend(3));
}

Also by using extension type rather than static extension on existing type you would be less prone to issues relating to naming conflicts.

eernstg

eernstg commented on May 2, 2025

@eernstg
MemberAuthor

That's a very good point, @mmcdon20!

The core issue here is that an object pattern like DateTime(weekday: DateTime.saturday || DateTime.sunday) doesn't present an opportunity for a parameter default scope to kick in: The references DateTime.saturday and DateTime.sunday are constant patterns, and this proposal doesn't say anything about patterns (including constant ones). In particular, the names like weekday that we can use in the enclosing object pattern are not associated with the formal parameters of anything, they are names of statically known getters of the matched value.

What we'd need is actually the ability to customize the namespace which is used to perform a lookup for a dot-shorthand which is used as a constant pattern in an object pattern.

This could be declared using an in clause on the corresponding parameter of a setter (that's the most straightforward case, we just search for the constant pattern value in the namespace that we'd search in order to set a property like weekday in the first place).

However, there may not be a setter for the getter of interest (for example, DateTime.weekday is actually final), and in this case we could allow a final variable to have an in clause (this has already been discussed: it could be useful in order to declare a specific namespace for that property, rather than writing the same namespace in an in clause on every constructor parameter that sets this final variable). So specifying that in clause on a getter would be a relevant way to specify the choice of namespace that a constant pattern on that getter should use for dot-shorthand lookups as well.

We could even allow a getter written with get and a body to have an in clause as well, even though it might not make sense to say that it is ever initialized or assigned a new value, simply because this in clause could specify a namespace to search for constant patterns that are dot-shorthands.

So there's not really a need to invent any extra ways to specify the default scopes, we just need to allow constant patterns to take them from the corresponding getter.

unless default scopes also applies to return types ie external int in DateTime get weekday; then it does not apply to the situation

It could be external int get weekday in DateTime; (external shouldn't make a difference).

If extension type had support for an implicit constructor it would be like having a parameter default scope

True, but I'm afraid the choice of a different parameter type just so we can get a different static namespace would be somewhat disruptive. For example, it is probably going to make it inconvenient to perform automatic completion when the actual arguments for that parameter are written.

Sure, I want implicit constructors, but I'm not convinced that they will work sufficiently smoothly in this particular scenario.

rrousselGit

rrousselGit commented on May 2, 2025

@rrousselGit

If extension type had support for an implicit constructor it would be like having a parameter default scope.

// with implicit constructor
extension type implicit const Weekday(int _) implements int { ... }

void main() {
print(isWeekend(.saturday));
print(isWeekend(3));
}

Another day, another Union type request :D

Things like be much simpler if we could have:

bool isWeekend(int | Weekend value) => 
mmcdon20

mmcdon20 commented on May 2, 2025

@mmcdon20

The core issue here is that an object pattern like DateTime(weekday: DateTime.saturday || DateTime.sunday) doesn't present an opportunity for a parameter default scope to kick in:

Yes but note that you are allowed to use dot-patterns here on the type of the weekday getter which is in this case int.

The shorthand would work in any of the following scenarios:

  • saturday and sunday are defined directly on int (an unlikely change)
  • saturday and sunday are defined indirectly on int from a static extension
  • saturday and sunday are redefined as an enum and the weekday getter returns the enum type.
  • saturday and sunday are moved to an extension type on int and the weekday getter returns the extension type. (as described in my previous comment above)

The references DateTime.saturday and DateTime.sunday are constant patterns, and this proposal doesn't say anything about patterns (including constant ones).

The dot-shorthands feature works on constant patterns already, it just uses the type of the getter as the context type.

In particular, the names like weekday that we can use in the enclosing object pattern are not associated with the formal parameters of anything, they are names of statically known getters of the matched value.

Right the crux of the problem here is that you don't only want to define a scope on parameter inputs, but sometimes you want to use it on a return type such as with a getter.

What we'd need is actually the ability to customize the namespace which is used to perform a lookup for a dot-shorthand which is used as a constant pattern in an object pattern.

We do already to some degree, if you return a different type, you get a different set of dot-shorthand lookups, so by using a wrapper type you get a fresh namespace to put static members in.

This could be declared using an in clause on the corresponding parameter of a setter (that's the most straightforward case, we just search for the constant pattern value in the namespace that we'd search in order to set a property like weekday in the first place).

  1. I would find it very unintuitive if you needed to define the default scope of the getter in the setter.
  2. What if the thing you want a default scope for is a function return value (and not a getter)?

However, there may not be a setter for the getter of interest (for example, DateTime.weekday is actually final), and in this case we could allow a final variable to have an in clause (this has already been discussed: it could be useful in order to declare a specific namespace for that property, rather than writing the same namespace in an in clause on every constructor parameter that sets this final variable). So specifying that in clause on a getter would be a relevant way to specify the choice of namespace that a constant pattern on that getter should use for dot-shorthand lookups as well.

...

We could even allow a getter written with get and a body to have an in clause as well, even though it might not make sense to say that it is ever initialized or assigned a new value, simply because this in clause could specify a namespace to search for constant patterns that are dot-shorthands.

So there's not really a need to invent any extra ways to specify the default scopes, we just need to allow constant patterns to take them from the corresponding getter.

I think you might be focusing in too narrowly on constant patterns, you would expect the following to work also:

DateTime.now().weekday == .saturday

unless default scopes also applies to return types ie external int in DateTime get weekday; then it does not apply to the situation

It could be external int get weekday in DateTime; (external shouldn't make a difference).

...

If extension type had support for an implicit constructor it would be like having a parameter default scope

True, but I'm afraid the choice of a different parameter type just so we can get a different static namespace would be somewhat disruptive. For example, it is probably going to make it inconvenient to perform automatic completion when the actual arguments for that parameter are written.

Sure, I want implicit constructors, but I'm not convinced that they will work sufficiently smoothly in this particular scenario.

What would be the issue for completion? The dot shorthand would just lookup in the declared type.


If extension type had support for an implicit constructor it would be like having a parameter default scope.
// with implicit constructor
extension type implicit const Weekday(int _) implements int { ... }
void main() {
print(isWeekend(.saturday));
print(isWeekend(3));
}

Another day, another Union type request :D

Things like be much simpler if we could have:

bool isWeekend(int | Weekend value) =>

Good suggestion. One notable difference between union type and implicit constructor is that the union type would get dot-shorthand suggestions from both types, and the implicit constructor would get dot-shorthand suggestions only for its own type. However the union type would still be more fine-grained than using a static extension which would be prone to cross contaminate the dot-shorthand suggestions... so I imagine that union types would be good enough in most scenarios.

bool isWeekend(int | Weekday value) => ...;
bool isWinter(int | Month value) => ...;
rrousselGit

rrousselGit commented on May 2, 2025

@rrousselGit

I've mentioned this before, but IMO a dedicated class is the way to go instead of "default scopes".
I don't like the idea of overly relying on primitive types.

Lots of things could technically be stored as "just an int/BigInt/...". For example:

  • Color could just be a num that represents the hex code
  • DateTime and Firestore's Timestamp could be an integer of microseconds since epoch
    ...

Things like Weekday/Month/... or pretty much anything that was discussed in this thread feel the same.
They can be represented as just an int, but that doesn't mean we should.

The only argument I've seen used in favour of using int instead of a dedicated class is "we want to write setWeekday(3) instead of setWeekday(WeekDay(3)).
But by that standard, DateTime/Color shouldn't be classes, and we should be able to do color = 0xFFFFFF instead of color = Color(0xFFFFFF)

rrousselGit

rrousselGit commented on May 2, 2025

@rrousselGit

Thinking about it, but isn't the issue of setWeekDay(WeekDay(3)) resolved with dot shorthands?

I don't remember the exact syntax, but afaik we could do setWeekDay(.(3)).
It's not quite as nice as just passing 3 directly, but that could work.

mmcdon20

mmcdon20 commented on May 2, 2025

@mmcdon20

@rrousselGit setWeekDay(.(3)) is not allowed, the closest you can do setWeekDay(.new(3)).

eernstg

eernstg commented on May 9, 2025

@eernstg
MemberAuthor

@mmcdon20 wrote:

the crux of the problem here is that you don't only want to define a scope on parameter inputs, but sometimes you want to use it on a return type such as with a getter

Yes, I noticed that, too, and it would be a straightforward generalization of this feature to allow getters and variables to have an in clause. It was already proposed that an in clause should be supported on an instance variable v such that the static namespace selection could be automatically propagated to every constructor parameter of the form this.v (and it would also be implicitly propagated to the parameter of the implicitly induced setter for the variable, if any).

We do already to some degree, if you return a different type, you get a different set of dot-shorthand lookups, so by using a wrapper type you get a fresh namespace to put static members in.

Indeed, and that would have been an obvious way to proceed if DateTime was a new class which was being introduced now. But all the approaches that rely on using a different type (even an extension type) are breaking (unless we get some extra mechanism like implicit constructors that allows the required coercion to be implicit).

I would find it very unintuitive if you needed to define the default scope of the getter in the setter.

I would definitely prefer to allow an in clause on a variable to apply to the getter as well as the setter, and I'd probably want an in clause on a getter to be implicitly induced on the parameter of the corresponding setter, if any.

Declaring it on the setter parameter and letting it propagate to the getter may seem more contrived, but I wouldn't rule it out. It certainly makes sense to say that we can handle myVariable = .monday based on the setter parameter (that just the way the mechanism has been working all the time), and then it seems natural to allow switch (myVariable) { .monday => ..., ... }.

Anyway if it's too weird to propagate the in clause from the setter parameter to the getter then we can just put it on the getter.

What if the thing you want a default scope for is a function return value (and not a getter)?

Good question! We could use an in clause on the function as a whole, following the getter. However, this may be a case which is so marginal that it's better to avoid the extra syntax and the associated readability/familiarity cost.

you would expect the following to work also:

DateTime.now().weekday == .saturday

We could specify that the dot shorthand namespace for == should be determined from a getter using the in clause of that getter, if any, otherwise the return type of the getter. The current approach just handles the case where the left hand operand is an expression of type T, and then we use the static namespace of T to look up dot shorthands, and this would be a small generalization of that rule which is in line with the generalization of the treatment of actual arguments (use the in clause of the parameter, otherwise the type).

What would be the issue for completion? The dot shorthand would just lookup in the declared type.

The difficult case is when you do not use a dot shorthand. In that situation you'd be writing an expression of one type and the context would specify a completely different (probably unrelated) type. I don't know how much the editing experience would be inconvenienced by this mismatch, but I wouldn't be surprised if it's a non-trivial issue, at least.

the union type would still be more fine-grained than using a static extension which would be prone to cross contaminate the dot-shorthand suggestions

I don't think so: The static extensions Weekday and Month were used in the in clauses exactly because this allows us to keep them separate during dot shorthand resolution, in spite of the fact that they can also be looked up in DateTime as DateTime.monday and DateTime.january and in spite of the fact that they are all of type int.

eernstg

eernstg commented on May 9, 2025

@eernstg
MemberAuthor

@rrousselGit wrote:

I've mentioned this before, but IMO a dedicated class is the way to go instead of "default scopes".

As a basic rule in software engineering, it's good for readability and correctness when distinct concepts are distinguished by having different types. This means that we can design and control the relevant set of operations, and we can prevent logical mistakes that arise from inconsistent usage (e.g., assigning an int that means "so many pixels" to an int variable that means "a color")

So, in general: Definitely yes, we want dedicated classes (or other types) for distinct purposes!

In the particular case DateTime there is another consideration, too: This class has been around forever, and it would probably cause massive breakage if we suddenly change the types of several members of DateTime (new parameter types in methods and constructors, new getter return types, etc), because many existing invocations of methods and constructors do not use the named values (DateTime.january is considerably more verbose than 1). So we might want to equip those declarations with the ability to help developers getting the values in order without changing the type. That's what we can do with the parameter default scopes, ensuring that .monday is an OK argument to the weekday parameter and .january is an OK argument to the month parameter, and not vice versa. This is not as strict as using a new type, but it is also not as disruptive.

The only argument I've seen used in favour of using int instead of a dedicated class is "we want to write setWeekday(3) instead of setWeekday(WeekDay(3)).

I think the management of breaking changes is more relevant than this particular kind of abbreviation, especially now where we will surely find a way to enable dot shorthands also in this case.

However, we can certainly make a choice along another dimension: If we've decided that a particular concept should be modeled using a distinct type then we can choose to make it a reified type (like a class) or a non-reified type (like an extension type). The former is more expensive in terms of run-time resources (time and space), and the latter is erased at run-time (so we need to be more careful to preserve those types).

I noticed that there is a proposal to use an extension type, but there haven't been any proposals to use a class to model Weekday and Month.

But by that standard, DateTime/Color shouldn't be classes, and we should be able to do color = 0xFFFFFF instead of color = Color(0xFFFFFF).

Perhaps it would make sense to consider turning Color into an extension type with representation type int? Who knows how much faster this would make all Flutter applications? ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    brevityA feature whose purpose is to enable concise syntax, typically expressible already in a longer formdot-shorthandsIssues related to the dot shorthands feature.featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @bernaferrari@Abion47@lukepighetti@jakemac53@lrhn

        Issue actions

          Parameter default scopes · Issue #3834 · dart-lang/language