Open munificent opened 2 years ago
- treat the argument expression as a static getter or method call on the referenced type.
If the referenced type is not in scope at the call site is it an error?
Separately, I wonder whether it would be worth considering generalizing this to arbitrary receivers of some sort? That is allow instance receivers as well?
I don't know if I understand this proposal well...
You gave an example of Flutter color parameters:
One of the common areas where users are frustrated by redundancy is color parameters in Flutter:
var myStyle = TextStyle(Color: Colors.red);
However, if we have to explicitly declare the class/enum with the from
keyword, it's entirely up to the Flutter API to decide which class/enum may be used as namespace. This isn't a viable solution for the problem of Flutter color parameters (or similar), at least not for all cases.
Considering the example you gave, TextStyle
, the following rises:
TextStyle
is a general Flutter component, and Flutter has no general colors class. Which namespace would the TextStyle
color
parameter opt in to?Colors
class. However, it belongs to the material
library. It would be inconsistent for TextStyle
to opt in to Material colors, as TextStyle
itself is not a Material component;Scaffold
backgroundColor
or Theme
color properties) to opt in to Material colors;Colors
namespace-like class with our design system colors. We use these colors for everything in our project, but we wouldn't be able to use them in non-material components like TextStyle
, Container
, ColoredBox
, which is, at least in our case, the vast majority of cases.The only way I see to workaround this, considering the current stable Dart + this proposal, would be to extend the class and opt in to whatever we want in it, which IMO is cumbersome and annoying to maintain:
class MyTextStyle extends TextStyle {
const MyTextStyle({
bool inherit = true,
Color? color from MyColors,
Color? backgroundColor from MyColors,
double? fontSize,
FontWeight? fontWeight,
FontStyle? fontStyle,
double? letterSpacing,
double? wordSpacing,
TextBaseline? textBaseline,
double? height,
TextLeadingDistribution? leadingDistribution,
Locale? locale,
Paint? foreground,
Paint? background,
List<Shadow>? shadows,
List<FontFeature>? fontFeatures,
TextDecoration? decoration,
Color? decorationColor from MyColors,
TextDecorationStyle? decorationStyle,
double? decorationThickness,
String? debugLabel,
String? fontFamily,
List<String>? fontFamilyFallback,
String? package,
TextOverflow? overflow,
}) : super(
inherit: inherit,
color: color,
backgroundColor: backgroundColor,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
letterSpacing: letterSpacing,
wordSpacing: wordSpacing,
textBaseline: textBaseline,
height: height,
leadingDistribution: leadingDistribution,
locale: locale,
foreground: foreground,
background: background,
shadows: shadows,
fontFeatures: fontFeatures,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
debugLabel: debugLabel,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
package: package,
overflow: overflow,
);
}
@mateusfccp, I think you're last request may be a bit out of scope. The other proposals I've seen are to implicitly rename .foo
, when the context type is T
, to T.foo
. All that to say it's reasonable to expect this shorthand to only work when there's one obvious type involved. If you want to use your own class or namespace, you might as well just use it in the call-site. I don't see a way to to signal to Dart that this is the class you want to use that's more concise or readable than just spelling it out at the call-site.
@munificent, I like this idea, I just have a few concerns:
I agree that a leading .
or some other character should be used to signify that the identifier belongs to a namespace and isn't -- and can't clash with -- an in-scope variable
it is a little irksome that API authors have to opt-in to this. Like you said, it does make it less brittle and puts breaking changes in the hands of the people who know when they will happen, which are both important. But it also means that callers need to wait until the function signature is updated before they can use it, and as was pointed out, there may be issues where the most commonly-used namespace isn't in the declaration scope, but is in the calling scope. Overall, not important issues, just matters of convenience
will this allow multiple namespaces to be provided? I imagined a comma-separated list similar to with
, but I don't know if that's grammatically separate enough from the following parameter. Also, there will most likely be a case where two namespaces have clashing identifiers, and then there's ambiguity as to the type, so maybe this shouldn't be supported
will users still be able to pass other valid instances, even if it's not the type specified in the from
? As already mentioned, this can be crucial when the namespace only represents a small subset of valid values.
@Levi-Lesches I didn't make any request. Instead, I commented on what was proposed based on what it was supposed to solve. The proposal's author specifically stated that the proposal should solve one problem, and I questioned how would it solve this specific problem considering some limitations.
If the referenced type is not in scope at the call site is it an error?
I don't think so. What matters is that the type is in scope where the API is defined. That's where the linkage is established. Then, from there, the parameter implicitly makes the names on that type in scope in the argument location.
However, if we have to explicitly declare the class/enum with the
from
keyword, it's entirely up to the Flutter API to decide which class/enum may be used as namespace.
That's correct and is a limitation of the proposal.
This isn't a viable solution for the problem of Flutter color parameters (or similar), at least not for all cases.
Considering the example you gave,
TextStyle
, the following rises: ... Flutter has aColors
class. However, it belongs to thematerial
library. It would be inconsistent forTextStyle
to opt in to Material colors, asTextStyle
itself is not a Material component;
That's a really good point. A consequence of this proposal is that it means that the API accepting the shorthand must directly couple itself to the namespace where those identifiers are looked up.
- For example, I work in a project that, as this proposal itself exemplify, has a custom
Colors
namespace-like class with our design system colors. We use these colors for everything in our project, but we wouldn't be able to use them in non-material components likeTextStyle
,Container
,ColoredBox
, which is, at least in our case, the vast majority of cases.
In cases where you want to have your own set of short names for argument values, you can always simply define a bunch of top-level variables/constants in a library and import it wherever you want to use them.
- will users still be able to pass other valid instances, even if it's not the type specified in the
from
? As already mentioned, this can be crucial when the namespace only represents a small subset of valid values.
Yes, this wouldn't be defining a restricted enumerated set of values, just a set of shorthands you can use to refer to some of them.
Is this a proposal that would fit a lot of use cases ? It seems like it's mainly aimed at flutter.
Is this a proposal that would fit a lot of use cases ? It seems like it's mainly aimed at flutter.
I think Flutter definitely exacerbates the problem since the Flutter API leans really heavily on named parameters and has a lot of parameters of enum or enum-like types.
How could we define such a namespace for a parameter of type List<Color>
?
Alternativelly couldn't we have this notion of namespace in a similar way as import/export?
For instance:
namespace Colors for Color;
f() => Button('One', alignment: .top, color: .darken(.blue));
(.top
is ok because implicitely A is a namespace for A)
or directly in a library defining:
export namescape Colors for Color;
NB : I like the dot prefix because it avoids naming conflicts and it is explicit about the usage of a namespace.
This might not be the right place for this, but for me function parameters came to mind, and I do like the idea of the prefixed dot:
color: Color.value
vs color: .color
feels similar to
foos.map((Foo foo) => foo.field)
vs e.g. foos.map(=>.field)
However,
(f) => f.x
(vs =>.x
).=>.
might also not be a good fit.It's not clear what the from
scope hangs on in language semantic model.
Is it part of the function type? If so, does it affect subtyping? Probably not, so most likely it's not part of the function type.
More likely it's static metadata carried along with the function type by the static inference and type propagation system, but completely absent at run-time. A kind of extra inferred knowledge about the parameter, but not something inherent to the parameter of the function type itself. Any function type can carry the metadata, and the method signatures of interfaces, or the signatures of static functions, which is declared with the feature would have the metadata from the start. We'd have to figure out how it works with joining function types from separate paths.
Or it could be a feature which only works when calling an interface method (where we know the class declaring the interface) or static method (where we know the function declaration directly), not on a function value.
~I think it could be better to use "in" as the keyword, instead of "from"~ never mind, that's pretty confusing.
I'm a huge fan of this proposal, hopefully this or something similar is integrated into the language at some point.
I have a different take, why don't we allow nested ;statements? For example
class Top {
class Inner {
static const int value = 1;
}
static const int value = 2;
}
void main(List<String> args) {
print(Top.value); // 2
print(Top.Inner.value); // 1
}
And we allow a private class to be declared within any structure, for example
void main(List<String> args) {
class MainInner {
static const int value = 1;
}
print(MainInner.value); // 1
}
I think nested classes will make the code cleaner, more readable, and easier to maintain. And we can use shorter names without worrying about naming conflicts.
This is a strawman proposal to address (some of) issue #357.
Here is an example of it in use. Say you have a couple of data types:
You also have this separate namespace-like class for working with colors:
Then you have a class that uses these:
To create an instance of this class today, you have to write:
You would like to be able to write:
This strawman enables that. But to turn it on, you need to change the Button and Colors classes like so:
The
from
clauses after the marked parameters are how those parameters opt in to special lookup rules for bare identifiers in arguments. When evaluating an argument expression:from
clause on the parameter, thenThe
from
clause lets an API author deliberately opt in to a set of identifiers that become valid arguments for that parameter. An API author can sort of say "here is the enumerated set of short names this parameter accepts".Pros
Compared to other proposals, this strawman more explicit and verbose. That explicitness provides a couple of benefits:
Identifiers can be looked up on another type
One of the common areas where users are frustrated by redundancy is color parameters in Flutter:
But, as you can see here,
red
isn't a property on the actual Color class, it's a constant on a separate Colors class. An explicitfrom
clause lets an API deliberately redirect to a separate type like that, like the example here shows.The namespace can be custom-tailored to the API
In fact, API authors can define their own custom namespace-like classes containing exactly the identifiers they want for a specific parameter. Any given parameter can have its own little purpose-built autocomplete namespace.
For example, you could do:
Here, the parameter's actual type is
int
, which isn't specific to any particular domain. There's no way we're going to add the ASCII table to the int class itself in order to have nicer looking charCode arguments.But since the author of a parameter chooses which type to look up argument shorthands on, this
fromCharCode()
constructor can point to a type specific to the API's domain.APIs are less fragile
The API author knows that changing the
from
clause can be a breaking change to uses of the API. If we rely on the parameter's type to determine which identifiers are allowed, then changing a parameter type is always a breaking change, even in ways that aren't breaking today.For example, say we decide that Button should also allow a string of CSS for its color. That means loosening the type of
color
to both Color or String:If the parameter's type determined what identifiers could be used, every existing callsite could have just broken. But by specifying the type that identifiers are looked up on explicitly, the API author can loosen the type while still preserving the lookup on Colors:
The parameter's type has changed, but every existing callsite relying on lookup on Colors continues to work. By not making the identifier lookup implicit based on the parameter's type, we give API authors more freedom to change parameter types without breaking users. They can evolve the parameter's type and its lookup namespace independently.
Cons
There are some negatives, though:
The syntax is strange and somewhat verbose. We've already packed quite a lot into the parameter list grammar, and this adds even more.
The benefit only applies to APIs that have opted in. If we roll out this language feature, it doesn't help any API users until the API maintainers have taken the time to update their parameter lists to take advantage of it.
The syntax only helps arguments. Other proposals based on context type can apply in any expression position where a context type may be available, like assignments, collection literals, etc.
We could fairly easily extend this strawman to support the right-hand side of binary operators (so
==
would work). Other syntactic locations are harder because it becomes less clear what API you should query to figure out what type to look up the names on.