Description
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 typeE
. IfP
does not have a default scope clause thenin 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 typeE
which is an enumerated type. In this case a default scope clause of the formin 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 formC
,C?
,C<...>
, orC<...>?
, whereC
is an identifier or a qualified identifier that denotes a class, mixin, mixin class, or an extension type. Assume thatC
declares a static member namedid
or a constructor namedC.id
. In that situation.id
is replaced byC.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
toT.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
[-]Parameter default scopes?[/-][+]Parameter default scopes[/+]eernstg commentedon May 24, 2024
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 typeEdgeInsets
. 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:
Use with this proposal:
Definitions:
Example 2: Alignment
Use current:
Use with this proposal:
Definitions:
Named constructors
Example 1: BackdropFilter
Use current:
Use with this proposal:
Definitions:
Example 2: Padding
Use current:
Use with this proposal:
Definitions:
Static members
Use current:
Use with this proposal:
Definitions:
rrousselGit commentedon May 24, 2024
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:
It is unclear to me how we could handle
fn<Color>(Colors.red)
here.eernstg commentedon May 24, 2024
Good points! Let me try to soften them a bit.
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
, addingin E
.Next, the mechanism could be introduced gradually for any other usages. For example, adding support for
blur
and otherImageFilter
constructors could be done for parameters of that type, and call sites in new code could then be less verbose than existing call sites.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 forMyColors
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 ofColors
, 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 useColors.crimson
with a different meaning, because each of you would import one of those static extensions, not both.Finally, for the generic case:
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 intoColors.red
in a very large number of locations (like, "in every location where the context type isColor
") is more serious than the convenience of being able to cover cases likefn<Color>(red)
can justify. This is particularly true because the type argument which is passed tofn
is probably going to be inferred, not explicit.cedvdb commentedon May 24, 2024
This could be implied and the default
Which would be the same as
eernstg commentedon May 24, 2024
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 commentedon May 24, 2024
@eernstg I believe your example is not what you meant to write in static members
color: Colors.green
should begreen
.imo, keep the dot
.
in front of the shorthand, it's more readablejakemac53 commentedon May 24, 2024
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 commentedon May 24, 2024
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:
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 commentedon May 24, 2024
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()
orEdgeInsets.all()
.100% agree. For EdgeInsets,
.all()
is a lot more readable thanall()
, 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 commentedon May 24, 2024
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
eernstg commentedon May 24, 2024
@cedvdb wrote:
True, thanks! Fixed.
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 commentedon Jun 14, 2024
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.
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?
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:
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 extendString
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
andpassword
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.
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.
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.
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.
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.
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 ofMyMaterialColors
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
orMyMathConstants
, 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.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 viaColor.red
is such a big deal?You call it a hook, I call it a crutch for a poorly implemented mechanism.
Not entirely accurate since Dart implemented final classes, but I digress.
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.
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:
Address the issues, argue them as incorrect, disregard them as unimportant, do what you will with them. It no longer concerns me.
mmcdon20 commentedon May 1, 2025
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:
Here we would ideally like to shorten the code to the following:
The problem here is that
weekday
is a getterexternal int get weekday;
, so unless default scopes also applies to return types ieexternal 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.
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.
If extension type had support for an implicit constructor it would be like having a parameter default scope.
Also by using
extension type
rather thanstatic extension
on existing type you would be less prone to issues relating to naming conflicts.eernstg commentedon May 2, 2025
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 referencesDateTime.saturday
andDateTime.sunday
are constant patterns, and this proposal doesn't say anything about patterns (including constant ones). In particular, the names likeweekday
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 likeweekday
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 anin
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 anin
clause on every constructor parameter that sets this final variable). So specifying thatin
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 anin
clause as well, even though it might not make sense to say that it is ever initialized or assigned a new value, simply because thisin
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.
It could be
external int get weekday in DateTime;
(external
shouldn't make a difference).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 commentedon May 2, 2025
Another day, another Union type request :D
Things like be much simpler if we could have:
mmcdon20 commentedon May 2, 2025
Yes but note that you are allowed to use dot-patterns here on the type of the
weekday
getter which is in this caseint
.The shorthand would work in any of the following scenarios:
saturday
andsunday
are defined directly onint
(an unlikely change)saturday
andsunday
are defined indirectly onint
from astatic extension
saturday
andsunday
are redefined as anenum
and theweekday
getter returns the enum type.saturday
andsunday
are moved to an extension type on int and theweekday
getter returns the extension type. (as described in my previous comment above)The dot-shorthands feature works on constant patterns already, it just uses the type of the getter as the context type.
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.
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.
...
I think you might be focusing in too narrowly on constant patterns, you would expect the following to work also:
...
What would be the issue for completion? The dot shorthand would just lookup in the declared type.
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.rrousselGit commentedon May 2, 2025
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 codeDateTime
and Firestore'sTimestamp
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 writesetWeekday(3)
instead ofsetWeekday(WeekDay(3))
.But by that standard,
DateTime
/Color
shouldn't be classes, and we should be able to docolor = 0xFFFFFF
instead ofcolor = Color(0xFFFFFF)
rrousselGit commentedon May 2, 2025
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 commentedon May 2, 2025
@rrousselGit
setWeekDay(.(3))
is not allowed, the closest you can dosetWeekDay(.new(3))
.DateTime
constants for month and weekday into extension types. dart-lang/sdk#60669eernstg commentedon May 9, 2025
@mmcdon20 wrote:
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 anin
clause should be supported on an instance variablev
such that the static namespace selection could be automatically propagated to every constructor parameter of the formthis.v
(and it would also be implicitly propagated to the parameter of the implicitly induced setter for the variable, if any).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 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 anin
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 allowswitch (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.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.We could specify that the dot shorthand namespace for
==
should be determined from a getter using thein
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 typeT
, and then we use the static namespace ofT
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 thein
clause of the parameter, otherwise the 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.
I don't think so: The static extensions
Weekday
andMonth
were used in thein
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 inDateTime
asDateTime.monday
andDateTime.january
and in spite of the fact that they are all of typeint
.eernstg commentedon May 9, 2025
@rrousselGit wrote:
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 anint
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 ofDateTime
(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 than1
). 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 theweekday
parameter and.january
is an OK argument to themonth
parameter, and not vice versa. This is not as strict as using a new type, but it is also not as disruptive.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
andMonth
.Perhaps it would make sense to consider turning
Color
into an extension type with representation typeint
? Who knows how much faster this would make all Flutter applications? ;-)