-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Parameter default scopes #3834
Comments
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 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. EnumsExample 1: BoxFitUse current: Image(
image: collectible.icon,
fit: BoxFit.contain,
) Use with this proposal: Image(
image: collectible.icon,
fit: .contain,
) Definitions: class Image extends StatefulWidget {
final BoxFit? fit;
const Image({
super.key,
required this.image,
...
this.fit,
});
}
enum BoxFit {
fill,
contain,
...
} Example 2: AlignmentUse current: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [ ... ],
) Use with this proposal: Row(
mainAxisAlignment: .center,
mainAxisSize: .min,
children: [ ... ],
) Definitions: class Row extends Flex {
const Row({
...
super.mainAxisAlignment,
...
}) : super(
...
);
}
class Flex extends MultiChildRenderObjectWidget {
final MainAxisAlignment mainAxisAlignment;
const Flex({
...
this.mainAxisAlignment = MainAxisAlignment.start,
...
}) : ...
}
enum MainAxisAlignment {
start,
end,
center,
...
} Named constructorsExample 1: BackdropFilterUse current: BackdropFilter(
filter: ImageFilter.blur(sigmaX: x, sigmaY: y),
child: myWidget,
) Use with this proposal: BackdropFilter(
filter: .blur(sigmaX: x, sigmaY: y),
child: myWidget,
) Definitions: class BackdropFilter extends SingleChildRenderObjectWidget {
final ui.ImageFilter filter;
const BackdropFilter({
required this.filter in ui.ImageFilter,
...
});
}
abstract class ImageFilter {
ImageFilter._(); // ignore: unused_element
factory ImageFilter.blur({
double sigmaX = 0.0,
double sigmaY = 0.0,
TileMode tileMode = TileMode.clamp,
}) { ... }
} Example 2: PaddingUse current: Padding(
padding: EdgeInsets.all(32.0),
child: myWidget,
), Use with this proposal: Padding(
padding: .all(32.0),
child: myWidget,
), Definitions: class Padding extends SingleChildRenderObjectWidget {
final EdgeInsetsGeometry padding;
const Padding({
super.key,
required this.padding in EdgeInsets,
super.child,
});
}
class EdgeInsets extends EdgeInsetsGeometry {
...
const EdgeInsets.all(double value)
: left = value,
top = value,
right = value,
bottom = value;
} Static membersUse 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 It also hard-codes those short-hands in the package ; when users may want to define their own shorthands. 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 |
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 Next, the mechanism could be introduced gradually for any other usages. For example, adding support for
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 This makes a specification like Finally, for the generic case: void fn<T>(T value); For the invocation I think the danger associated with a very broad mechanism that would enable |
This could be implied and the default
Which would be the same as
|
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 |
@eernstg I believe your example is not what you meant to write in static members imo, keep the dot |
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. |
I agree that 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
100% agree. For EdgeInsets, This being an opt-in feature with the |
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:
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. |
@jakemac53 wrote:
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 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:
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, #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:
The main difference is that the top-level function would pollute the name space much more pervasively: Every occurrence of With the mechanism proposed here we would only be able to call So, for example, this mechanism would allow many different constructors whose name is of the form |
What is the type of the expression |
@tatumizer (removed my previous comment before you quoted me but) Type Same as type 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 |
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 : In principle, with static interfaces, you can do something like this: @SimulateEnum()
class Colors {
static Color red = ...
static Color blue = ...
...
} and make |
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. |
Nothing beats |
@StarProxima:
This doesn't allow Color values defined in the class |
Is it really necessary to support the It would be weird to support only the 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 #357. |
The return type of |
@tatumizer wrote:
That's actually quite different: If the parameter type is a union type, say With This means that the default scope and the parameter type are decoupled, which is useful in the case where the parameter type is used for different purposes in different APIs. For example, you might want to use some named values as arguments to a certain parameter of type
Well, I did mention them already in the very first version of the initial comment on this issue. Static extensions are highly relevant (because they support extensibility), but the language team hasn't committed to add them to the language so I can't promise a 100% that we will get them. |
@Abion47 wrote, about the syntax
Ah, of course! Thanks for catching that! I adjusted the proposal to omit the support for multiple default scopes. This discussion has shown that this kind of feature is actually better handled via static extensions (the static extensions would then be used to populate a single default scope, possibly from multiple sources).
I'm not sure what you mean by 'proactive extensions'. The clause I don't see how that could be described as 'extending types'...
The library developer can just choose to have a declaration that does not specify a default scope, for example The ability to specify a default scope which isn't the parameter type allows library authors to have more control when they want it. Nothing is forcing an On the other hand, I think it's quite likely to be a viable approach to use a separate declaration as the default scope for It would presumably be empty as declared, and it would be populated by clients. They could add in the specialized colors like
Static extensions can add static members to
If Flutter makes the choice to use If Flutter uses
Obviously you wouldn't do that. I don't think anybody ever mentioned the idea that a declaration like However, Flutter/widgets does already (and should) depend on dart:ui, so there's no problem in the fact that the declaration of > class Container extends StatelessWidget {
> Container({Color color}); // The default scope for colors is `Color`.
> } If Flutter makes the choice to use a separate declaration as the default scope for a set of colors then it would be declared in dart:ui. I've used the notation Anyway, I'll use a different name for the thing that I've called 'ui.Colors', just to make it even more explicit that it is not the same thing as // --- dart:ui
class Color {...}
class DefaultNameSpaceForColors {} // Empty, populated by static extensions.
// --- google3/third_party/dart/flutter/lib/src/widgets/container.dart
class Container extends StatelessWidget {
Container({Color color in DefaultNameSpaceForColors});
}
First, there is no reason whatsoever to have the dependencies that you mention. Next, it is no problem at all to populate the chosen namespace, no matter which approach you're considering. If you are using // Third party library.
import 'dart:ui';
static extension on Color {
... // Put static members and/or constructors into `Color`.
} If instead Flutter uses a separate namespace to hold colors then we'd do this: // Third party library.
import 'dart:ui';
static extension on DefaultNameSpaceForColors {
... // Put static members and/or constructors into `DefaultNameSpaceForColors`.
} Where do you see 'every combination of Colors, CupertinoColors, and any other collection type of Color that exists'? I don't see anything like that. There's no need to update something that doesn't exist, either.
But then we are in complete agreement on the possible existence and even legitimacy of such expressions. This implies that we can also be justified in discussing the software engineering properties of expressions with that typing structure. You seem to claim that we can only discuss language design decisions based on excerpts from production code, it's never appropriate to discuss code with the same structure that happens to lay out this structure using 10 times fewer lines of code?
That's true. You could argue that there's some semantic noise in the That would be an argument in favor of using examples where we declare a bunch of extra classes, just so we can illustrate a point about a typing structure without relying on numbers. It is going to be considerable more verbose, though.
You don't mean 'familiar'? For example, I tend to believe that every language mechanism comes with a certain burden of getting to know what it is and how it works. In any case, it's very easy to change the keyword (or keyword sequence) in the default scope clause. I'm all for anything that works (and that discussion will include considerations about readability as well as conciseness). About context types:
if (foo is Bar) {
foo = Foo.create();
} We don't have enough context to be able to say how this works. (No pun intended, of course ;-) If In particular, So the context type for the expression
Well, we should note that the declared type could very well be rather general. For example class FooBase {}
class Foo extends FooBase {
Foo();
Foo.create();
}
class Bar extends Foo {}
void f(FooBase? foo) {
if (foo == null) foo = Bar();
if (foo is Bar) {
foo = Foo.create();
// `foo` now has type `FooBase`.
}
} What I'm saying is simply that (1) it is a non-trivial exercise to find the exact static type of I'm sure we can find some rules, and they might even work OK. I just think it's going to be too hard to reason about, and too hard to maintain, if we allow these flimsy types obtained from promotion or from type inference to be used for static member lookups. So I'm recommending that we warn against them.
class State {
State();
factory State.working() = WorkingState;
factory State.invalid() = InvalidState;
}
class WorkingState extends State {}
class InvalidState extends State {}
void main() {
var state = getCachedState();
if (state is InvalidState) {
state = State.working();
}
initialize(state);
}
// Glue code, allowing the code above to compile.
State getCachedState() => throw 0;
void initialize(State _) => throw 0; I don't see a problem, this is working code (when
So you'd want First, I'd prefer to warn against this, thus effectively recommending an explicit Next, if we do embrace the use of promoted types as default scopes then I'm not convinced that the declared type will always be the preferred default scope. For example: sealed class State {
State();
factory State.working(int value) = WorkingState;
factory State.invalid() = InvalidState;
}
class WorkingState extends State {
final int value;
WorkingState(this.value);
}
class InvalidState extends State {}
void main() {
var state = getCachedState();
if (state is InvalidState) {
state = State.working(10);
} else if (state case WorkingState(:var value)) {
if (value < 15) {
state = State.working(value + 1); // Or `state = .new(value + 1)`.
}
}
initialize(state);
}
// Glue code: Just enough to allow the code to compile.
State getCachedState() => throw 0;
void initialize(_) => throw 0; With the approach that I've recommended (emitting a warning when a promoted or inferred type is used as a default scope), we would be able to use |
@tatumizer wrote here:
Right, the rule that we're using the class/mixin/etc. With a formal parameter we can make the choice to use any other namespace, e.g., class C { const C(); }
class CShortcuts { static const theC = C(); }
void f(C c in CShortcuts) {}
void main() {
f(.theC); // OK.
switch (C()) in CShortcuts {
case theC: ...;
default: ...;
}
var anotherC = CShortcuts.theC; // OK.
C anotherC = .theC; // Error, context `C` is not enough to bring up `CShortcuts`.
C yetAnotherC = CShortcuts.theC; // Redundant, but we have no fix for this now.
} I've thought about it, and it might be possible to provide a more general kind of support for using a separate namespace in other situations as well, but it is not obvious that it could be done in a way which is sufficiently expressive and at the same time comprehensible. So there's no support for such things in this proposal. One issue that comes up is that if we associate Next, if we don't declare class CShortcuts provides_a_default_scope_for C {
static const theC = C();
} This exactly what a parameter declaration like But that will work without any specification saying that We could invent an additional syntax for the variable declaration (e.g., |
(FYI: I'm on vacation for a bit more than a week now, so I'll only be able to respond here on Wed Jun 12 or so.) |
That is correct, I got my wires crossed with the example I had posted a few days ago. This is what I meant to do: class Foo {
static final bar = Bar();
}
class Bar extends Foo {}
void main() {
final Foo a = .bar;
final b = Foo.bar;
} The issue comes from static fields, not factory constructors. In this example, |
@lrhn: in principle, would it be appropriate to use class annotation, say: @shortcuts(Color) // provides shortcuts for Color class
class ColorShortcuts {
const red = Color(someValue);
const blue = Color(otherValue);
// etc.
} I admit I don't understand the status of annotations in dart. The docs say the annotation is for the tools only, but the nature of some annotations like |
Annotations have no status in the Dart language itself. They're allowed to exist, but the language assigns no meaning to annotations other than them needing to be valid constants. So an annotation cannot change anything, it's just a signal to tools, which can choose to do something on their own, mainly refuse to continue in some cases, but they're not allowed to change the runtime semantics of valid programs. |
I understand why you did it, but that just makes this feature even more nonsensical if it doesn't support multiple scopes. It whittles the effective use case of this feature to when you want to extend the scope of a type in a parameter but only in a handful of places and only using a single target type. I'm still of the opinion that this proposal is largely a solution in search of a problem, but that use case is so hyperspecific that, even if I had been 100% agreeing with you on its importance, I would wonder if so much work was worth it for such a narrow payoff.
By proactive/retroactive, I am referring to extending a type for use with the shorthand syntax either "proactively" (i.e. at the source) or "retroactively" (i.e. after the fact). For instance, your proposal is a proactive extension: void foo(Color color in Colors) { ... } This approach proactively adds support for members of Conversely, static extensions are a retroactive extension: extension MaterialColorExt on Color {
static final Color red = Colors.red;
...
} This approach retroactively adds support for members of
I think this fact is one of the biggest personal issues I have with this proposal. It's generous to even call this feature syntactic sugar as it is effectively just an IntelliSense assistance tool. The latter half of
That is not the point. The point is that when people do want to use
I have no idea what this is in response to. The context of my quote was such that it was proactive extensions (and hence your proposal) that isn't user extensible. I don't know what that has to do with the ability to add static members to
Between this and the previous quote, I think some wires got crossed. When I was listing my issues, those were issues with your proposal of
If I understand this correctly, your idea is that there is some Here are some issues I can see with that, in no particular order:
The point was that using your Basically, here's what I'm getting at. The more we hammer out these issues with your proposal, the more you suggest shoring those issues with static extensions. At some point, you have to wonder what exactly is gained from the additional complexity of
I mean, not really? This may be a special case due to the involvement of numeric literals, but using that to suggest we shouldn't be using numeric literals at all (or wrapper classes at the very least) is a bit of a leap. I'm not saying that we should only use production-quality code when considering use cases for feature proposals (because we shouldn't). I'm saying that the circumstances behind this specific example code are so specific, contrived, and antithetical to best practices that I have difficulty drawing any meaningful conclusions from it.
No, I meant self-evident. Someone who can generally understand programming languages but had no experience with Dart could read that code and make a pretty good guess what it does. Familiarity does play a part in that ability, but so does the ability to logic through a vaguely English structure. (And like I already said, the use of But again, my point wasn't that features shouldn't be written if they aren't readable without becoming familiar with it (though, in fairness, one would need a very good reason to implement a feature that wasn't). My point is that the
Essentially, there is a discrepancy here between the context of the promotion and the context of the expression: class Foo {
factory Foo.create() = Bar;
}
class Bar extends Foo {}
void main() {
var Foo foo;
foo = ???; // A
if (foo is Bar) {
foo = ???; // B
}
} Looking at line A, the "context type" of the right hand side of the expression is the base type of any value that would be accepted in that position. In this case, that aligns with the computed context type of Looking at line B, the "context type" hasn't changed, since any value of This discrepancy makes no sense to me. If it is statically known that the root accepted type of the assignment is void foo(num n) {
if (n is double) n = 1;
// ...
} The problem was whether the 1 in |
I see no reason something like this wouldn't be possible. The only thing is that it would require the annotation to be paired with a code generation pass that detects and parses the annotation, then generates an extension to |
At that point, why not just write the extension directly, and name it static extension ColorShortcuts on Color {
static const red = Color(someValue);
static const blue = Color(otherValue);
// etc.
} |
The problem I'm trying to solve is this: how to tell the compiler to ignore all static methods defined in class C, but take only those defined in the extension? The idea is to keep the list of shortcuts "clean" - let them contain only what I really use, without the accidental stuff that was never intended to serve as shortcuts. |
The whole idea of static extensions is to add functionality to a type. What you're talking about is in essence hiding or removing functionality from a type. Making These shortcuts aren't extending the language with any new functionality. All they do is add syntactic sugar for the sake of convenience. Tacking on features like customizing how it actually works is, in my opinion, making it far more complicated than it needs to be. |
Swift, which serves as a source of inspiration for the feature, hides stuff. Based on their proposal (which I cited earlier), they provide shortcuts only for members that return a base type (they even say "exactly" a base type). |
To be fair, if all the Dart team did was |
The difference is that it "hides" members by only showing the members that match the context type. So, for instance, if you had a class that had a static member of a different type, that member wouldn't be accessible using the shorthand syntax: class SomeClass {
static var shared = SomeClass()
static var label = "SomeClass Label"
func f() -> SomeClass { return SomeClass() }
}
@main
struct App {
static func main() {
let x: SomeClass = .shared // Allowed
let x: SomeClass = .shared.f() // Also allowed
let x: SomeClass = .label // Not allowed, also non-sensical
}
} This "hiding" of static members is just a natural consequence of the shorthand syntax. If the variable type is But what you're talking about is manually designating members of a type to not be included in the autocompletion, and that's another matter entirely. To my knowledge, Swift does not allow that. |
Here are the initial findings of my research into swift's shortcuts Swift allows you to use the shortcuts liberally. class Vehicle {
static let car = Car()
static let c=Vehicle.car // OK
static let d:Car=.car // error: '=' must have consistent whitespace on both sides
static let e:Car = .car // OK
static let f = .car // error: reference to member 'car' cannot be resolved without a contextual type
static let g = car // OK! no dot, not prefix is required in static context
}
class Car: Vehicle {
} (Please notice the quirk on the Swift supports extensions. An extension for class Vehicle can be declared, rather unimaginatively, as Every static method defined in the extension can be called via the shortcut without limitations. (recommended playground: https://www.programiz.com/swift/online-compiler/ ) |
In an attempt to find out why other languages are in no rush to copy this "shortcuts" feature, engaged myself in research. Here's what people say. There are two main advantages of the method:
The annotation controlling the IDE treatment of dot might be as expressive as we wish. (BTW, using |
I removed support for plain identifiers, only The reason for this change is that it is too difficult to read code where any plain identifier could be looked up in the context type (that is, in the namespace provided by the class which is the context type, etc.). The form |
@Abion47 wrote, 2 weeks ago:
Did you read the proposal? What is it that you want and that you can't express using this proposal? I'll try to find the answer to this question as I'm reading your comments, but a bit of help would be awesome!
This doesn't make sense. The parameter whose name is There is no change to the set of acceptable arguments at all. The clause abstract class Colors {
static const red = MaterialColor(...);
...
}
void main() {
f(.red); // OK, means `f(Colors.red)`.
} Some other proposals use additional criteria to filter the available members of the given default scope. For example, it has been a requirement that we only get to use members whose type is the given context type (here If we want to enable chains (like
I'd very much like to have static extensions, and as you may know this proposal has used them from day one, based on the hope that we'll get them (somewhat soon, too). So we don't have to choose one or the other, I'm arguing that they work well together. I think your term 'retroactively' covers pretty much the same concept as when I say 'extensible'. The point is that a client (who doesn't have the ability to edit a class like
That's true. Support for adding this kind of imports would be great, and I think we'll have better support for that over time, module priorities and resources.
You obviously can't ignore the clause
I can have points, too. ;-)
You're right, that could take several hours. It's a one time investment, though.
You said that parameter default scopes do not allow for extensibility, I responded that static extensions are a perfectly effective tool to make parameter default scopes extensible. You can use the type itself (that is, put stuff into The point is that the different namespace can be less crowded, because it only plays the role as a provider of named distinguished values that we're expected to want to pass to specific parameters, and the class itself may have many static declarations that do not have this purpose.
If you wish to use the class itself (such as No big deal. When you write an
Exactly. This is one possible approach. It does involve some machinery (and some work, initially), but it allows clients to populate that empty namespace in any way they wish.
It's a hook, that is, a mechanism that allows clients to add functionality to something, e.g., a big framework. Another example of a hook is a virtual method (in Dart: every class/mixin/enum instance method is virtual, but extension methods are not). The Template Method design pattern is an example where it is very clearly used as a hook. Is that an antipattern?
That is, when the extension is imported into the library that uses the additional members. 'dart:ui' certainly does not have to import a static extension in order to use those additional members in some client library. Just like ordinary extension methods.
OK, choose a different name. The point is how it is used.
Both are possible. Using a separate namespace is more work and more control, that might be preferable.
If we are using a separate namespace then the compiler cannot possibly guess that any particular namespace is intended to be used in this way. We have to declare it. That's what the
That is indeed a possible outcome, and then you'd just rely on the context type to deliver the default scope. Depends on whether or not this makes
As I mentioned, static extensions have been part of this proposal from day one. Extensibility is important!
The context type of any expression in Dart is well defined. It's been around for several years. We don't get to invent a new meaning for that concept willy-nilly, because that would basically break all programs. Context types are also not trivial, and demotion of promoted variables is one of the tricky cases. At line A, the context type of the right hand side of the assignment is So the context type of the right hand side of the assignment in line B is If the type of that right hand side is not a subtype of void foo(num n) {
if (n is double) n = 1;
// ...
} The int-to-double coercion example is different. The reason why this coercion takes place is a specific rule in the language specification: An integer literal that occurs such that the context type |
@rrousselGit wrote:
You could do that, if you aren't worried about overcrowding |
@tatumizer wrote:
That's the main motivation for doing some extra work and putting those distinguished values (static variables, or static methods, or constructors) into a separate namespace. |
I think "overcrowding" is what some people actively want. Take one of my packages: Riverpod But I've seen multiple people prefer having all of their "providers" hosted as abstract class AllProviders {
static final a = Provider(...);
static final b = Provider(...);
} They do so because they explicitly wish to have a way to see the list of all objects of a given kind in the same place, for the sake of autocompletion. So I think many people would actively prefer seeing all colours under |
We've had a couple of decades where the word 'aspect' was associated with a lot of discussions about software elements that are 'scattered' or 'tangled', corresponding to the situation where we'd want them to be located in one place (such that they are easier to find), or where we'd want to have two different elements in different places because they are dealing with different 'concerns'. Static extensions actually touch on the same topic area: They make it possible for some static declarations or constructors to be declared in a different location than the one which is logically their "home". So we could use them to populate // --- Library 'widely_used_lib.dart';
abstract class AllProviders {} // Just an empty container. Pure extensibility.
// --- Library 'a.dart'.
import 'widely_used_lib.dart';
static extension on AllProviders
static final a = Provider(...);
// --- Library 'b.dart'.
import 'widely_used_lib.dart';
static extension on AllProviders {
static final b = Provider(...);
}
// --- Library 'lib.dart'. Populate `AllProviders`, in a reusable way.
export 'widely_used_library.dart';
export 'a.dart';
export 'b.dart';
// --- Library 'my_program.dart'.
import 'lib.dart';
var useIt = AllProviders.a; // OK. This approach might be considered as scattered, or it might be considered as the opposite (cohesive?): For the client, it looks like So if people really want the physical overcrowding then they'd put everything into I'm suggesting that colors could be a good example: 'dart:ui' cannot possibly depend on 'material.dart' or 'cupertino.dart', but it can hold an empty bucket that everybody knows about, and clients can populate in any way they wish. |
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? // in package:my_company_common/authorization.dart
Future<SignInResult> signIntoUserPool(String pool, String username, String password) { ... } Imagine this is a function in a company's internal generic common library. In an implementation that references it, they might have several pre-defined pools relevant to that application: // in package:my_company_client_app/.../auth_service.dart
import 'package:my_company_common/authorization.dart';
abstract class ClientUserPools {
static const workers = 'workers';
static const teamLeads = 'team-leads';
static const supervisors = 'supervisors';
}
void signInWorker(String username, String password) {
final result = await signIntoUserPool(ClientUserPools.workers, username, password);
...
} It would be nice if they could shorten Also, what about the 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 And that's just for one type - if I also wanted to make use of
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
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. |
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)
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.
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:Assume that
E
is an enumerated type that declares the valuev
ande
is an expression whose static type isE
. An expression of the forme == .someName
(ore != .someName
) will then resolve ase == E.someName
(respectivelye != 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
.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.The text was updated successfully, but these errors were encountered: