Open eernstg opened 1 month ago
Checking this proposal against the cases in this comment.
The main issue to discuss here is probably that we will fix at the declaration of each formal parameter that supports this kind of abbreviation from which scope it can be made available.
For example, there is a case below where a member has type EdgeInsetsGeometry
, but the actual argument has type EdgeInsets
. I've addressed that by including support for both of those scopes, but it gets harder if we wish to enable many scopes.
A counter point would be that we can add static extensions to the language, and this would allow us to add extra members to existing scopes.
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,
...
}
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,
...
}
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,
}) { ... }
}
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;
}
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');
...
}
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.
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.
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}) {
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.
@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
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.
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.
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.
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
}
@cedvdb wrote:
color: Colors.green
should begreen
.
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.
@jakemac53 wrote:
users might want to be able to opt out
That should not be necessary: Anything that currently has a meaning will continue to have that meaning (because we're using the standard scope rules). So you'd just write what you would write today, and it would never trigger this mechanism.
If you are OK with the new syntax, then instead of in
clause or with
clause (which every other user will forget to add), we can target the root cause by allowing the syntax like
class Colors simulates Enum<Color> {
static Color red = ...
//etc
}
What would be the difference between defining a global method with the same signature as one defined in the class if we don't keep the leading .
?
EdgeInsets all(double value) {
// ...
}
void foo({required EdgeInsets padding in EdgeInsets}) {
// ...
}
foo(padding: all(16));
@jakemac53 wrote:
what if we just had a more general feature to add static members into the top level scope?
This could add a lot of names to the top-level scope. It might be difficult to manage them and avoid name clashes. We could consider local imports, https://github.com/dart-lang/language/issues/267. That is definitely one way to provide direct access to a set of names in some other scope (it's got one vote at this time ...).
@Reprevise wrote:
What would be the difference between defining a global method with the same signature as one defined in the class if we don't keep the leading
.
?
The main difference is that the top-level function would pollute the name space much more pervasively: Every occurrence of all
would then resolve to a declaration (the top-level function that you mention, or some nested declaration that shadows it).
With the mechanism proposed here we would only be able to call all(...)
when the particular formal parameter admits the transformation (for example, from all(...)
to EdgeInsets.all(...)
).
So, for example, this mechanism would allow many different constructors whose name is of the form *.all
to coexist. With a top-level function you'd have to choose one of them.
I should be able to do the following : String status in 'on' | 'off' .
What is the type of the expression on | off
?
Isn't it a kind of Enum?
What if you want to declare a type with two values on
and off
?
What syntax will you use?
@tatumizer (removed my previous comment before you quoted me but)
Type 'on' | 'off'
Same as type Colors.green | Colors.red | ...
which can be generated from static members of Colors by a macro or the language ( with an "in" keyword for example). How the subset is generated is a detail.
Enum shorthand syntax is a different feature imo, but the two seem to be conflated in the proposal.
I personally don't like this proposal. It would be 5x easier to just convert enums to strings like TS and support "contain", with no scoping problem, and union being easier as a bonus. I think what most people want is "left" | "center" | "right" (right now as an enum, but if it were an union type you wouldn't need to remember the class name, so win-win scenario). Swift is nice beause you type "." and it suggests the available types. You don't need to remember anything, just the ".". Similarly, TypeScript is nice because you type " and it suggests the available types.
In your proposal you loose this super important aspect, there is no way to type "something" and ask for the analyzer to suggest the options.
I agree that one advantage of having the .
prefix is it gives a good thing for autocomplete to work off of. I had the exact same thought.
In your proposal you loose this super important aspect, there is no way to type "something" and ask for the analyzer to suggest the options.
Note that autocomplete works with for example "ctrl + space" too without having to type anything but it may propose more options than necessary without "."
@cedvdb : And now you have 2 different concepts formalizing the "fixed set of values": one is the (existing) enum, and another is ... What do you call the type of 'on' | 'off'? If it's not an enum, then what is it? A second concept, parallel to enum? But if it is an enum, then you have to somehow shoehorn it into an existing concept of enum.
In principle, with static interfaces, you can do something like this:
@SimulateEnum()
class Colors {
static Color red = ...
static Color blue = ...
...
}
and make SimulateEnum
macro add implements static Enum<Color>
and define all the remaining Enum methods.
But we don't have a parametrized class Enum<T>
today.
How is this proposal different from this one?
The existing design proposal seems more thoughtful. Also, I agree that having starting points can simplify typing with autocomplete, also it would help avoid name collisions.
Note that autocomplete works with for example "ctrl + space" too without having to type anything but it may propose more options than necessary without "."
Nothing beats "
or .
. Still half the muscle than ctrl+space (which changes depending on OS, machine and keyboard). " and . are always consistent.
For me this is unbeatable:
@StarProxima: In the existing proposal, there's a restriction
We restrict to static members returning the same type as the type declaration they’re on
This doesn't allow Color values defined in the class Colors
to be used with the context type Color.
@tatumizer
Is it really necessary to support the Colors
class? In theory, we can get colors from many places, from our own class with static fields, from ThemeExtension
, directly using the constructor...
It would be weird to support only the Colors
class for the possibility of shorthand.
Supporting enum, static fields and constructors (static methods?) already covers most use cases and is fairly obvious without requiring changes to existing code to support shorthand for use.
I would vote for the existing design proposal to address this issue https://github.com/dart-lang/language/issues/357.
@eernstg
If we want to allow Colors
class, perhaps we could add some annotation to support the use of Colors
whereverColor
is required (perhaps on the declaration of the Color
class itself, rather than on each use)? I think that would be easier than introducing new syntax into the language.
@tatumizer
If it's not an enum, then what is it? A second concept, parallel to enum?
What is the theoritical type of Color color in Colors
in the proposal anyway ? It's obviously not Color
but a subset. In that way, String text in 'on' | 'off'
is not different, as a subset of String (implementation details aside).
@jakemac53 wrote:
the
.
prefix is it gives a good thing for autocomplete to work off of
That's a very strong argument in favor of having the leading .
. I'm looking into the grammar in order to try to learn some more about the impact of allowing '.' <identifier>
as a <primary>
.
@StarProxima wrote about the proposal named 'Dart enum value shorthand' from @lrhn:
How is this proposal different from this one?
You could say that all these proposals (we've discussed a bunch of them, some more elaborate than others, over a period of several years) are concerned with the selective access to namespaces that are not otherwise available.
(I'll use 'namespace' to refer to the static declarations of a class, mixin, extension type, extension, etc, because it's just a mapping from names to values that we are using.)
In particular, it allows id
or .id
(or some other variation of that theme) to have the meaning E.id
because we have a reason to look for the name id
in the namespace associated with E
.
We could do that directly by opening the scope (that's a local import, #267). We could do it by putting a .
in front of the identifier (that's https://github.com/dart-lang/language/blob/main/working/3616%20-%20enum%20value%20shorthand/proposal-lrhn.md). We could do it by declaring that specific locations will provide the namespace (such as this proposal).
(By the way, I don't think there would be anything wrong with a proposal which is similar to this one, but a '.' in front of an identifier would be used to trigger the mechanism. That should work just fine together with the in E
mechanism. It could probably be optional, and it could be used to insist that we should use the extra namespace even in the case where the given name is already declared somewhere in an enclosing scope. I'm just worried about the syntactic cost ...)
So that was the trigger mechanism: How do we gain access to that extra namespace?
The next question is which namespace we're talking about. Several proposals rely on the context type, including 'Dart enum value shorthand'. In other words, for an actual argument to a function/method/constructor invocation, we look up the parameter type and use that in order to select the namespace. Concretely, if the parameter type is Color
then we find the class denoted by the identifier Color
(that is, the one which is in scope at the location where the formal parameter is declared), and then we try to find something named id
in that namespace.
In contrast, this proposal relies on a user-specified namespace. For example, for a parameter of type Color
we can make the choice to specify a namespace like the class Colors
, which will then be used to look up id
.
One reason why I've chosen to use an explicitly specified namespace is that this allows us to have specific choices for a specific context. For example, if we want to provide symbolic names for a set of magic numbers then we can do this:
extension MagicNumbers on Never { // An extension on `Never`: This is nothing but a namespace
static const theBestNumber = 42;
static const aBigNumber = 1000000;
static const aNegativeNumber = -273;
}
void f(int number in MagicNumbers) {...}
void main() {
f(theBestNumber); // Means `f(42)`.
f(14); // Also OK.
int i = 0;
f(i); // Also OK.
}
If we were to insist that the chosen namespace for every int
must be the int
class then we wouldn't be able to have specific "application domains" for the same type, we'd have to stick to very specific sets of values like a specific enum, which would then be used in exactly the same way whenever it's used at all.
It is quite important for this proposal that we can have something like static extensions to provide extensibility.
static extension on MagicNumbers {
static const aRandomNumber = 87;
}
void main() {
f(aRandomNumber + theBestNumber);
}
Surely, static extensions would also be useful in collaboration with 'Dart enum value shorthand'.
Another thing to note is that we can offer values of more than one type in the given namespace.
extension MyNumbers on Never {
static const int numberOne = 1;
static const double numberHalf = 0.5;
}
void f<X extends num>(X x in MyNumbers) {}
void main() {
f(numberOne); // Means `f<int>(1)`.
f(numberHalf); // Means `f<double>(0.5)`.
}
This illustrates that it is (1) possible to provide values of different types in the same namespace (the call site chooses a name like numberOne
and hence the caller is in control with respect to the chosen value and type); and (2) the chosen type could influence type inference.
Another way to use the ability to have multiple types (rather than relying strictly on the context type) is that this allows us to use subtypes of the context type. For example, both numberOne
and numberHalf
can be passed as arguments to a function whose parameter is num n in MyNumbers
.
Finally, the explicitly declared namespace allows for combinations of namespaces num n in MyNumbers, MagicNumbers
. Presumably, it would be OK for those namespaces to have conflicts, which would be resolved in a standardized manner (for example, if name
has two different declarations then it's the last namespace that wins). This is again not so easy to do if we rely on the context type as the only and final selector of the namespace.
@cedvdb wrote:
What is the theoritical type of Color color in Colors in the proposal anyway?
As the title may hint, this proposal is about providing some default values from a specified namespace. The type of color
is Color
, and any instance of type Color
can be passed. But we expect that certain values of that type are particularly useful, and hence we'd like to provide support for denoting them in a concise manner. The role played by said namespace is to contain declarations of those "extra useful" values.
As always, we may also find a static method m
in said namespace, and we may have an invocation like m([1, 2])
which turns out to mean MyNameSpace.m([1, 2])
. This means that we have a set of "extra useful" functions, but we can also choose to pass any "normal" expression whose type fits the given parameter.
Also, constructs like switch (e) in E { ... }
have yet another variant of similar properties.
@eernstg Snap, my mind immediately jumped on a feature I wished for. So it would not be a compilation error to pass a Color that is not in Colors despite the fact that I'm declaring Color color in Colors
. It's just an hint or "default values". In retrospect that makes sense for the Colors case...
Although "in" is a loaded term, especially in the DB world, ence the confusion, well that and the fact that I read the proposal diagonally.
@StarProxima : upon reflection, I see that the Colors
class is an outlier. Had these "named colors" been defined in the class Color directly, they would comply with the restrictions of the proposal you cited - problem solved. Why aren't they there?
My guess is that Color is a general-purpose class defined in dart:ui, but Colors comes from flutter:material and defines color names according to the spec of Material. (Other standards may have different color names/values).
The simplest way to make these colors available for shortcuts is to define
class MaterialColor extends Color {
const red = MaterialColor(0xFFFF0000); // just copy/paste the definitions from Colors using a script.
const blue = MaterialColor(...);
...
const MaterialColor(int value) : super(value) {}
}
Then the restriction is satisfied. Another (better?) option could be: do nothing and leave Colors alone :-)
The opt-in requirement is a massive turn off. Any new feature like this that requires opt-in is a feature that may as well not exist as the vast majority o. It's also a massive problem for any library where the function is defined in a separate package than the type:
// Canvas class from dart:ui
class Canvas {
void drawColor(Color color, BlendMode blendMode);
}
// Colors, MaterialColor from flutter/material
class Colors {
static const red = MaterialColor(....);
static const orange = MaterialColor(....);
static const yellow = MaterialColor(....);
static const green = MaterialColor(....);
static const blue = MaterialColor(....);
static const purple = MaterialColor(....);
}
class MaterialColor extends ColorSwatch<int> {}
There is no way for Canvas.drawColor
to say it supports from Colors
without adding a tight coupling from the dart:ui library to the flutter/material library and there is no extension syntax that would allow the flutter/library to patch the function with support. And even if a proposal added such syntax syntax, it would require a significant amount of fragile boilerplate and code duplication to achieve this functionality.
Also, what if someone had their own CustomColors
list that they wanted to be able to use? This proposal doesn't allow for specifying more than one source type after the in
and any solution I can think of would add sources of ambiguity. And even ignoring that, the developer would have to manually extend every function that supports Colors
to add support for CustomColors
which would get problematically tedious very fast.
I'm sorry, but this proposal is a complete non-starter IMO. It solves very few aspects of the original issue while adding a fair amount of entirely new issues, and every way I look at it, it adds far more work than it saves.
I think the danger associated with a very broad mechanism that would enable red to be transformed into Colors.red in a very large number of locations (like, "in every location where the context type is Color") is more serious than the convenience of being able to cover cases like fn
(red) can justify. This is particularly true because the type argument which is passed to fn is probably going to be inferred, not explicit.
I personally don't want fn(red)
, but instead fn(.red)
, where that .red
is replaced with Color.red
(not Colors.red
. This would then have to be combined with static extensions, to have Colors
values be available on Color
).
The difference is pretty big. It is much simpler to understand what's happening and does not conflict with other possible top-level red
or this.red
variables.
As such, I disagree that a broad mechanism to do this is actually dangerous.
Wow, absolutely everybody hates this proposal! 😁
Well, I've made some adjustments. First, I've experimented with the grammar and concluded that it isn't so dangerous after all to allow '.' <identifier>
as a primary. So that's now included. It is used to force the lookups in the default scope (that is, it forces the given identifier to be looked up in the declared default scopes, disabling all the local scope rules).
Next, it seems likely that this kind of lookup will always be useful when the associated type is an enumerated type. Hence, when E myParameter
is encountered and E
is an enumerated type, it is implicitly changed to E myParameter in E
. Similarly for switches whose scrutinee has a static type which is an enumerated type.
enum MainAxisAlignment {
start,
end,
center,
spaceBetween,
spaceAround,
spaceEvenly;
}
void f(MainAxisAlignment alignment) {
switch (alignment) {
case .start: ...;
...
case .spaceEvenly: ...;
}
}
Finally, a number of detailed issues in the semantics have been made more precise. In particular, actual type arguments can not be passed explicitly to a constructor invocation because those type arguments are passed to the class part of the constructor name, not the "last name". We might be able to find a good syntax for that, but for now we just have to write the constructor invocation like today, without any abbreviations.
@cedvdb wrote:
Snap, my mind immediately jumped on a feature I wished for. So it would not be a compilation error to pass a Color that is not in Colors despite the fact that I'm declaring
Color color in Colors
. It's just an hint or "default values". In retrospect that makes sense for the Colors case...
Right, it's a "default scope". In other words, if the name isn't in scope locally then we can go to the default scope and find it. When the prefix period is used, we must find the name in the default scope.
About the word in
: It is indeed rather vague. We could use default in
rather than just in
. On the other hand, requests for brevity are more common than requests for long, self-explanatory sequences of keywords. ;-)
@tatumizer wrote:
Had these "named colors" been defined in the class Color directly, they would comply with the restrictions of the proposal you cited - problem solved. Why aren't they there? My guess is that Color is a general-purpose class defined in dart:ui, but Colors comes from flutter:material and defines color names according to the spec of Material. (Other standards may have different color names/values).
Exactly, so why don't we just embrace the notion that we can have a widely used type (like Color
), and we may want to provide easy (abbreviated) access to a specific subset of instances of that type, and then we may (obviously!) want to use more than one such subset.
@Abion47 wrote:
The opt-in requirement is a massive turn off.
I guess this is a reference to the fact that the original proposal required an in E
clause on the given parameter in order to do anything at all?
This has been adjusted such that enumerated types always get an in Something
: We can declare it explicitly if we want something special, otherwise we just use the enumerated type itself as the default scope. So you shouldn't be turned off that massively with the new updates. ;-)
There is no way for
Canvas.drawColor
to say it supports fromColors
without adding a tight coupling from the dart:ui library to the flutter/material library and there is no extension syntax that would allow the flutter/library to patch the function with support. And even if a proposal added such syntax syntax, it would require a significant amount of fragile boilerplate and code duplication to achieve this functionality.
Those are well-taken points!
It is not a trivial exercise to allow for extensibility in any mechanism, and there are so many examples of research whose goal is to support one or the other kind of extensibility. Dependency management comes up frequently. We probably don't have any proposals that are perfect in this respect.
However, I believe there is a way forward:
Assume that Dart adds support for static extensions (#723 has 742 upvotes today).
We could then provide the colors in the class Colors
as members of a static extension. As far as I can see, nobody outside colors.dart
can use the class Colors
for any other purpose than looking up static members.
We could then consider changing the material Colors
to be a static extension. It might be a static extension of Color
in dart:ui, but it could also be a static extension of a new declaration named Colors
in dart:ui (if we're afraid that Color
would get too crowded).
So let's assume that we introduce that new declaration named Colors
in dart:ui, and then change the material Colors
to a static extension that populates the dart:ui Colors
:
// dart:ui
class Canvas {
void drawColor(Color color in Colors, BlendMode blendMode);
}
extension Colors on Never {
// A home for a bunch of colors, added via
// `static extension on Colors {}`.
// This could be empty, it would rely on some other
// library to populate this namespace with some colors..
}
// Colors, MaterialColor from flutter/material
export 'dart:ui' show Colors; // To avoid breakage, if needed.
static extension MaterialColors on Colors {
static const red = MaterialColor(....);
static const orange = MaterialColor(....);
static const yellow = MaterialColor(....);
static const green = MaterialColor(....);
static const blue = MaterialColor(....);
static const purple = MaterialColor(....);
}
class MaterialColor extends ColorSwatch<int> {}
With this approach there is no dependency from dart:ui to material.dart, we just need a namespace (Colors
in dart:ui) that we can populate from anywhere using static extensions.
@eernstg : please look at this comment and below.
This idea (inspired by swift) allows us to achieve essentially the same result with a much simpler design. There's no need to do much about the existing declarations; the rules are simple: given a context type Foo, you are allowed to write .method
for any static (this includes constructors) method
defined in Foo (the latter is not necessarily being an enum type). And for Colors
, you have to just formally re-classify the existing Colors class as a static extension.
It's not that your design is bad or something. It's just more complicated than the alternative, so the cost/benefit ratio is higher.
It's much easier to explain to the user that when the context type is Foo, the expression starting with .id
is equivalent to Foo.id
- the rest follows from here.
Supporting the chain is necessary, otherwise, you can say color: Colors.red.withOpacity(0.5)
, but won't be allowed to say color: .red.withOpacity(0.5)
I find it surprising how many people want Colors, specially since Flutter uses Material 2014 palette which is super bad and even Google has been recommending not using it for years. Tailwind has a much better palette, and has changed colors 4 times on the past 4 years. Swift has also changed colors a few times, and now has like 4 variations for each color (supporting dark theme, high contrast, or both).
I would say, focus on enums first and let colors for later. My biggest fear is slowness, specially with Material Icons, where typing "." might or might not slow down things while it searches for all possible icon choices.
The switch example is good, I like it. But you are almost into unions territory. My dream is still this being possible one day:
void f("start" | "center" | "end" alignment) {
switch (alignment) {
case "start" : ...;
case "center" : ...;
case "end" : ...;
}
}
I would say, focus on enums first and let colors for later.
Enums alone are not enough. In Flutter, there are lots of "enum-like" classes (Alignment
is a representative example).
They cannot be made into enums because they implement a general concept, but provide a number of frequently used predefined constants, which is what everybody uses most of the time. Supporting only enums will leave the users wondering why in 50% of cases they can write .id
, and in the other 50% cannot. The whole motivation for this enhancement is to do something about cases like alignment: Alignment.bottomCenter
, which is clearly redundant.
The reason for fixation on Colors is that 1) it's not the only class exhibiting the problem (e.g. Icons is similar), 2) if the enhancement cannot cope with these classes, then again, the users will be wondering... (see above)
My biggest fear is slowness, specially with Material Icons, where typing "." might or might not slow down things while it searches for all possible icon choices.
The speed will be the same as it was before; the dot will be just a shortcut to Icons.
, and IDE will have to do the same amount of work (they most likely cache the results anyway).
@bernaferrari
I find it surprising how many people want Colors, specially since Flutter uses Material 2014 palette which is super bad and even Google has been recommending not using it for years. Tailwind has a much better palette, and has changed colors 4 times on the past 4 years. Swift has also changed colors a few times, and now has like 4 variations for each color (supporting dark theme, high contrast, or both).
I would say, focus on enums first and let colors for later. My biggest fear is slowness, specially with Material Icons, where typing "." might or might not slow down things while it searches for all possible icon choices.
The switch example is good, I like it. But you are almost into unions territory. My dream is still this being possible one day:
void f("start" | "center" | "end" alignment) { switch (alignment) { case "start" : ...; case "center" : ...; case "end" : ...; } }
It's not that people are specifically wanting Colors
. It's simply a common example of the way the Flutter SDK is designed, with static members of classes that may or may not be related to the target class. Another example is Padding
which takes a property of type EdgeInsetsGeometry
for the padding
parameter but the most common use case is to call a factory constructor of the derived EdgeInsets
class. A third is Icons
, which is a container for many static instances of IconData
but is itself unrelated to the IconData
class. A proposal for the static dot syntax has to account for all of these use cases, because if it doesn't, the feature will be either unusable or inconsistent within Flutter, at which point, why even bother?
(And you may point out that this wouldn't have been a problem had Flutter been designed differently, but that complaint is entirely irrelevant and unhelpful. It is what it is, and now we simply have to deal with it.)
Also, type unions are an entirely different (and extremely complex) beast that have nothing to do with this feature, and it's not like the features are mutually exclusive.
You are right, I forgot about padding
Different non overlapping defaults could be supported something default in X | Y
. Just a thought, not sure if useful
Unions don't really solve the underlying problem.
I personally struggle to see why we don't implement "using the .identifier
shorthand in a context where the parameter is of type T is strictly equivalent to T.identifier
".
This has been suggested many times, but we seem to be talking around the subject. What's the issue with it and why isn't it considered more?
Assuming that we'd support Colors
and such through static extensions, so that instead of Colors.magenta
we'd do Color.magenta
, which could use the .magenta
shortcut.
The rule is simple, and it's fairly flexible. We can support things like:
Padding(padding: .all(16));
Padding(padding: .only(left: 10, top: 5));
@tatumizer wrote:
This idea (inspired by swift) allows us to achieve essentially the same result with a much simpler design. There's no need to do much about the existing declarations; the rules are simple: given a context type Foo, you are allowed to write
.method
for any static (this includes constructors)method
defined in Foo (the latter is not necessarily being an enum type). And forColors
, you have to just formally re-classify the existing Colors class as a static extension.
I think the complete reliance on the context type works very nicely for enumerated types. That's also the reason why I made it the default for enumerated types: You basically always want to select a value from the complete set of instances of that type.
However, even for an enumerated type we could have situations where we want something different:
enum E { one, two, three, four }
abstract final class EvenE {
static const two = E.two;
static const four = E.four;
}
class C {
final E e;
C.anythingGoes(E this.e); // Use the default `in E`.
C.onlyEven(E this.e in EvenE); // Offer only the "even" values.
}
void main() {
C.anythingGoes(three); // OK.
C.onlyEven(two); // OK.
C.onlyEven(one); // Compile-time error.
}
The need to avoid putting every possible named value of a given type into the same bucket is of course much more acute with widely used types like String
or int
, as I mentioned along with the MagicNumbers
example earlier.
The point is that the context type as such is too broad to be the sole identification of a relevant set of named values—there will be formal parameters with the same type that do not have the same set of relevant named values, and it's going to burden us with some polluted name spaces if we insist that they must always go into the same bucket.
Colors
is actually a good example: They did not put all the material colors into Color
. I don't know exactly how those discussions went, but it might very well be because that would canonicalize this particular set of colors. There will be a Material 4 whose colors could be slightly different, and it's probably not going to look very good if you mix and match colors from different palettes.
Supporting the chain is necessary
As an experiment, I included support for selectors and cascades. See the end of this comment, too.
@bernaferrari wrote:
I find it surprising how many people want Colors, specially since Flutter uses Material 2014 palette which is super bad and even Google has been recommending not using it for years. Tailwind has a much better palette, and has changed colors 4 times on the past 4 years
To me this sounds exactly like we'd want support for choosing a set of relevant default values, not forcing everybody to use the same union-of-all namespace.
@cedvdb wrote:
Different non overlapping defaults could be supported
something default in X | Y
I think this would be the same thing as in X, Y
, which is supported in this proposal (assuming that X
and Y
denote classes or other entities that are capable of declaring static members).
@rrousselGit wrote:
I personally struggle to see why we don't implement "using the
.identifier
shorthand in a context where the parameter is of typeT
is strictly equivalent toT.identifier
".
In this proposal, that is indeed the default treatment of any parameter or switch scrutinee whose type is an enum
.
We could extend the semantics such that .identifier
(and derived forms like .identifier<num>(14).bar[0]
) would have the meaning T.identifier
(respectively T.identifier<num>(14).bar[0]
), but I don't think it's going to generalize very gracefully, in two respects:
_
, which is subject to type inference). What is the semantics if we try to look up .someIdentifier
in _
?.identifier
case.For example, who says that the static type of T.identifier<num>(14).bar[0]
is assignable to T
? Alternatively, are we supposed to track down a type T1
such that T1.identifier<num>(14).bar[0]
has a type which is assignable to T
?
For example, who says that the static type of T.identifier
(14).bar[0] is assignable to T? Alternatively, are we supposed to track down a type T1 such that T1.identifier (14).bar[0] has a type which is assignable to T?
I don't think that's a concern. We'd get an assignment error then.
If T.foo
isn't assignable to T
, fn(.foo)
would give the exact same error as fn(T.foo)
.
The syntax wouldn't mean "sugar for possible values of type T", but "When typing .foo
, T.foo
was inferred".
Similar to how we can type final foo = 42
and it gets inferred as final int foo = 42
. We know that the initializer has a context type of type int
, so we infer int foo
The context is not a type, it is a type schema (which is a type except that it may have occurrences of the unknown type, often written as , which is subject to type inference). What is the semantics if we try to look up .someIdentifier in ?
If there's no context type, then it should be a compilation error to use .foo
.
The type could not be inferred, so we emit an error.
What is the semantics if the context type is a type variable?
We can use .identifier
only on known type variables.
So we can't do:
void fn<T>(T value) {
fn<T>(.identifier); // Makes no sense, we cannot infer "T."
}
But we can do:
void fn<T>(T value) {
fn<int>(.parse('42')); // Valid. We know that `T` is of type `int`. So we infer `.parse` as `int.parse`.
}
Using analyzer terms, I'd implement this by relying on parameterElement
and staticElement
.
If they are null
, InvalidType
or TypeParameterType
, then using the .identifier
syntax results in a compilation error (due to the lack of contextual information).
Meaning we could do:
final int foo = .parse('foo');
int fn() => switch (.parse('foo')) {
...
}
Color c;
switch (c) {
case .red: ...
}
This includes more complex expressions too. Like:
final int foo = .tryParse('foo') ?? 42;
// Equivalent to:
final int foo = int.tryParse('foo') ?? 42;
Because int
the staticType
of this expression.
It is the users' responsibility to ensure that this expression will indeed return a value assignable to the expected type.
@eernstg: in other words, you are introducing constrained subtypes without calling them this way.
This opens Pandora's box. There are many cases where you'd want them, e.g. someone might be interested in small integers in the range 1..100
. Ada goes all the way in this direction: see this section, skip to 3.3.2.
I don't question the wisdom of introducing such types. But here, it all looks like shooting sparrows from cannons. and missing. A small issue of allowing .id
syntax becomes dependent on a very large issue, which is (nonetheless) unlikely to be able to address the problem at hand.
Suppose the user types color: Color.
and presses CTRL-SPACE. What suggestion is expected here?
Probably, all static methods and constants and constructors of Color, right? Even if Color class itself included a constrained subtype of possible colors that provided 100 named colors out of the box, this is not the reason for not showing a suggestion for the constructor Color.fromARGB(int a, int r, int g, int b)
.
It's much worse with flutter's Colors. They list just a number of colors preselected for Material scheme.
But you can't constrain the color:
parameter of any widget to these colors (something that in
clause suggests)
Someone may hate these colors and love CupertinoColors instead. But the widget is so constrained that it cannot understand CupertinoColors. The number of color palletes is practically unlimited - anyone can define their own pallette.
There's no way to handle this issue exhaustively to everyone's satisfaction. What I propose is a modest trick: re-formulating Colors into static extension, and then .red
will automatically mean at least something. If you don't like that, you can later change .red
to CupertinoColors.systemRed
. Or maybe Flutter can make CupertinoColors a static extension. We can't cover all potential options for color:
attribute anyway - e.g. ColorSwatch
has a constructor, but this constructor will hardly be found by IDE. But the users will hopefully understand the limitations.
For example, who says that the static type of
T.identifier(14).bar[0]
is assignable toT
?... If
T.foo
isn't assignable toT
,fn(.foo)
would give the exact same error asfn(T.foo)
.
The point I was making was that it seems silly to support chains of method and getter invocations in general if the resulting expression is unlikely to be type correct in the given context.
Assume that we encounter .identifier(14).bar[0]
at a location where the context is the plain class type T
(just to say that there's nothing hard about the context type schema itself in this case). The identifier
could be the name of a static member of T
, but it seems quite unlikely to me that T.identifier(14).bar[0]
has a type which is assignable to T
. So why would we transform it into T.identifier(14).bar[0]
in all cases, if that's just a type error in most cases?
It seems like it would be a much more useful mechanism if we could find an S
such that S.identifier(14).bar[0]
has a type which is assignable to T
. However, that's not so easy because we would need to "search all types" to find S
.
The Dart style guide has a rule that says avoid returning this
, and that basically makes it less likely that e.foo().bar[7].baz
has the same type as e
.
I think cascades are much more promising in this respect: We can definitely transform .identifier(14)..bar[0]
into T.identifier(14)..bar[0]
when T.identifier
is a constructor of T
or a static method whose return type is T
.
I don't know. I don't think transforming .identifier(14).bar[0]
into T.identifier(14).bar[0]
just because the context type is T
appears to be a beautiful or promising language mechanism.
Perhaps it's can work, and developers would just need to navigate the typing properties of those chains with care. But it does sound like a recipe for surprises and disappointments to me.
Similar to how we can type
final foo = 42
and it gets inferred asfinal int foo = 42
.
That's very different because in that case we're inferring a type int
which is guaranteed to preserve the type correctness.
Transforming .identifier(14).bar[0]
into T.identifier(14).bar[0]
because the context type is T
seems more like inferring final int foo = 42.isEven;
because 42
has static type int
. ;-) We're by design ignoring the type of the expression as a whole and just latching onto the leading token (.identifier
respectively 42
).
But we can do:
void fn<T>(T value) {
fn<int>(.parse('42')); // Valid. We know that `T` is of type `int`. So we infer `.parse` as `int.parse`.
}
(I assume that the invocation of fn
could be anywhere, it's just convenient to have it in the body of fn
because we need to have a declaration as well as an invocation of fn
. So it doesn't matter that it's an infinite loop.)
That's a good point!
That is a case where the context type is int
, and there is no reasonable way to specify in the declaration of fn
that the parameter uses a default scope. So that is definitely a case where it could be argued that it is convenient to support the transformation from .parse
to int.parse
based on the context type alone.
We could allow the form with the leading period to use the context type like this (that is, it would be allowed in all locations where there is a context type), if nothing else is specified. Of course, C<int>?
would yield C
if that's a class that declares any static members or constructors.
That should cover the following:
This includes more complex expressions too. Like:
final int foo = .tryParse('foo') ?? 42;
// Equivalent to:
final int foo = int.tryParse('foo') ?? 42;
The context type for .tryParse('foo')
is int?
, so we'd consider constructors/static members of int
.
@tatumizer wrote:
you are introducing constrained subtypes
Nono, there is no subtype relationship between E
and EvenE
, I'm just illustrating that if we don't recommend using every possible value from an enumerated type as an argument for a specific parameter then we can express that, too.
Suppose the user types
color: Color
. and presses CTRL-SPACE. What suggestion is expected here?
As of today, Color
does not contain a bunch of constant variables holding specific colors. It's probably a good idea to keep it that way. For example, I don't think it's going to be pleasant if that particular situation would cause all named colors in the entire imported world to pop up.
So the completion would yield just those static methods and constructors of Color
that you'd get today.
This is a pretty good reason why we don't want to limit ourselves to an unconditional 100% reliance on the context type. We want to make it possible for other namespaces than Color
to provide named colors.
It's much worse with flutter's Colors. They list just a number of colors preselected for Material scheme. But you can't constrain the
color:
parameter of any widget to these colors (something that in clause suggests) Someone may hate these colors and love CupertinoColors instead. But the widget is so constrained that it cannot understand CupertinoColors. The number of color palletes is practically unlimited - anyone can define their own pallette.
It is possible for dart:ui to support a customizable set of colors (as mentioned here):
// --- 'dart:ui'.
class Canvas {
void drawColor(Color color in Colors, BlendMode blendMode);
}
abstract final class Colors {
// A home for a bunch of colors, added by other libraries via
// `static extension on Colors {...}`.
}
// --- 'material.dart'.
static extension MaterialColors on Colors {
static const red = MaterialColor(....);
static const orange = MaterialColor(....);
static const yellow = MaterialColor(....);
static const green = MaterialColor(....);
static const blue = MaterialColor(....);
static const purple = MaterialColor(....);
}
// --- 'cupertino.dart'
static extension CupertinoColors on Colors {
...
}
If your program imports 'material.dart' then the material colors will be available when passing arguments to a Canvas
, and if you import 'cupertino.dart' then the cupertino colors will be available, and you can even have both (unless that creates a lot of other conflicts ;-).
There's no limit on the number of palettes you can have with this approach, and also no limit on the number of separate contributions you can add to ui.Colors
, if needed (you will just get a compile-time error if you've added two colors to the same namespace with the same name and then try to use that name).
What I propose is a modest trick: re-formulating
Colors
into static extension
Yes, that's exactly the approach that I had in mind as well. Of course, the details may be tricky to get right, and it's definitely not acceptable to cause widespread breakage.
@eernstg:
class Canvas { void drawColor(Color color in Colors, BlendMode blendMode); }
Maybe I misunderstand the meaning of this declaration, but to me, it reads like: "color parameter should be one of the colors defined in Colors subclasses and extensions on Colors, and only in these places". The meaning of in
, by its nature, is quite restrictive. When we say for (var i in [0, 1])
we really mean "all those values and nothing else". But color
parameter should be able to accept, among other things, the values like Color.fromARGB(...)
and, generally, any variables of type Color. How do you enable them? Static extension cannot declare constructors AFAIK.
Either I don't understand the meaning of "in", or the definition is too restrictive.
Another point is that any method with color parameter is now supposed to add in Colors
in their definition. There can be uncountable methods in flutter that accept color, and now what? All of them have to modify their signature?
Maybe we can just say somehow once: whenever the parameter is of type Color
, enable the logic of substituting the shorthands defined in Colors (and, by implication, extensions on Colors) with a shorter notation .id
?
(In general, it feels like you are moving in a right direction, but I can't understand the details, sorry)
Edit: how about
abstract final class Colors providing_shortcuts_for Color { // better wording is needed
// A home for a bunch of colors, added by other libraries via
// `static extension on Colors {...}`.
}
The idea of aggregating the shortcuts, and only the shortcuts, leads to a cleaner solution, but in
??? The above definition contains enough information to make in
unnecessary. It automatically applies to every parameter of type Color
. There can be several extensions providing shortcuts for Color (MaterialColors, CupertinoColors etc), and they can be imported selectively
@tatumizer I misunderstood also at first. It reads as default can be found in
, which I would abbreviate to default in
personally, certainly not just in
@cedvdb: but why? One extra clause in the definition of Colors class (see above) is enough to trigger the logic of substitution for every parameter and variable of type Color!
In response to https://github.com/dart-lang/language/issues/357:
Here is an idea that the language team members have discussed previously, but so far it does not seem to have an issue where it is spelled out in any detail.
It supports concise references to enum values (e.g.,
f(mainAxisAlignment: .center)
andcase .center:
rather thanf(mainAxisAlignment: MainaxisAlignment.center)
andcase 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 findcenter
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 scopeE
. For example, we can specify that a value of an enum typeE
can be obtained by looking up a static declaration inE
:It has been argued that we should use the syntax
T param default in S
rather thanT param in S
because the meaning ofin S
is thatS
is a scope which will be searched whenever the actual argument passed toparam
triggers the mechanism (as described below). This proposal is written usingin 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 formin E
will implicitly be induced. For example:We can support looking up colors in
Colors
rather thanColor
because thein E
clause allows us to specify the scope to search explicitly: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:We can also choose to use a completely different set of values as the contents of the default scope. For example:
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 (likeAcmeColors
), 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
.This feature allows us to specify a set of
int
values which are considered particularly relevant to invocations off
, and give them names such that the code that callsf
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 typeint
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 tof
or tog
.Proposal
Syntax
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 formE.id
, whereE
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 namen
to the declaration denoted byE.n
for each namen
such thatE
declares a static member namedn
.The constructor namespace of a default scope clause
in E
is a mapping that mapsn
to the constructor declaration denoted byE.n
for each namen
such that there exists such a constructor; moreover, it mapsnew
to a constructor declaration denoted byE
, if it exists (note thatE.new();
also declares a constructor whose name isE
).Consider an actual argument
.id
of the form'.' <identifier>
which is passed to a formal parameter whose statically known declaration has the default scope clausein E
.Assume that the static or constructor namespace of
in E
mapsid
to a declaration namedid
. In this caseid
is replaced byE.id
.Otherwise, a compile-time error occurs (unknown identifier).
In short, an expression of the form
.id
implies thatid
is looked up in a default scope.Consider an actual argument of the form
.id(args)
whereid
is an identifier andargs
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 intoE.id(args)
.Consider an actual argument of the form
.id<tyArgs>(args)
whereid
is an identifier,tyArgs
is an actual type argument list, andargs
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 ofid
, 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 intoE.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.
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 forme s
wheres
is derived from<selector>
will then be transformed intoe1 s
ife
will be transformed intoe1
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 forme .. s
ore ?.. s
which is derived from<cascade>
will then be transformed intoe1 .. s
respectivelye1 ?.. s
ife
will be transformed intoe1
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 ofargs
, etc.This feature is implicitly induced in some cases:
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.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..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.
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
.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..id
toT.id
when no other rule is applicable. Change the support for selector chains and cascades to a part of the proposal..id.foo().bar[14].baz
) and cascades as a possible extension.