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.

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

    Issue actions