-
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
Enum shorthand design parameters #4149
Comments
This looks good to me. I'm especially happy to see that there's no specification of an extra namespace in this version of the feature. It should be backward compatible to add that later if there's a strong enough need for it, but we can ship the most critical part of the feature much sooner without it.
That's a fairly critical requirement. The analysis server needs to be able to quickly compute the context type in order to keep code completion fast enough to be usable. The code completion support already has a notion of context type (used for ranking). We'll need to make sure that it's consistent with the definition in the spec (the two might have diverged) and that the definition in the spec can be computed efficiently enough for completion to work well.
I'm guessing that "functions" here means "static methods".
What do you mean by "static extension"? Is this a reference to the possible future language feature that I've heard referred to as "static extension methods", or do you mean something that's in the language today that I might know by a different name? |
Correct.
The hypothetical future "static extension methods" feature, which is an imprecise name if it also allows static extension variables, functions and constructors. |
Looks as a fascinating feature for Flutter development :-) The proposed subset looks reasonable to implement to me from analysis point of view. I liked reading about sealed subtypes or even just any imported subtypes. Could be useful to refer static getters, but for constructors (usually unnamed) not so much, the |
That's why you need a "pronoun" (like Doing it in a single step throws the baby out with the bathwater IMO. 😄 (As a bonus, you get a place to attach type parameters: |
@lrhn what's your personal current recommendation for extent? It looks like you're missing a bullet point. |
Nit: every mention of |
My personal current recommendation (differences from @lrhn's recommendation noted in bold):
Side note on that last bullet point: last week I was leaning toward "We recognize |
That was included in the "Syntax": One getter, one metod or one constructor invocation only. Not allowing a tear-off is not a necessity, but without a following selector chain, a function type will never be valid for a context type that has a static namespace. So they'll never work (unless the context type is only a suggestion, not a requirement). If we allow a full selector chain, there is no need to make restrictions, we'll just require the result to type-check in the end. |
Thanks for writing this up, it's really helpful. My perhaps somewhat controversial weakly-held opinion is to keep it as simple as possible and only support static variable/getter access. So just I like the idea of supporting functions and constructors, but I'm really worried it will lead to code like: Foo f = .new(.new(.new(), .new(1)), .new(.new(2, .new()))); Of course, we can simply say "Don't do that, it's bad style." But I'm not confident that will be enough. |
I wouldn't worry much about it. I would rather simply suggest it, and people do as they want. Just as today, one can name their variables with single letters, let they do what they want. Practically speaking, at least in the context of Flutter (and this is the way I program anyway, even when using pure Dart), named parameters are much more used than unnamed (unless they are like 1-parameter functions), and the parameter name usually gives a lot of context when reading the code. |
IMO being able to write For example, folks would likely want Material shades accessible in Color, to write: backgroundColor: .amber.shade100; I'd also have use cases for this in my packages, as a mean to improve the namespace. Currently in Riverpod, given: @riverpod
Future<Model> example(Ref) => ...; Folks can write: ref.watch(exampleProvider.future) But we could use this feature to instead write: ref.watch(.example.future) Riverpod folks regularly ask for a way to know what all the "providers" in the app are ; or complain about the verbosity of the trailing |
Here's an extra argument in support of this idea. |
I don't mind removing support for
This feature targets values, not Widgets.
But |
For this, as a minimum, dart has to introduce another form of factory that can return a subclass static extension on EdgeInsetGeometry {
const factory EdgeInset all = EdgeInset.all;
} (Not sure about the syntax). |
^ There are some separate issues for those:
I don't think they are mandatory to implement shorthands though ; as it's just boilerplate reduction. But they are good to have :) Off-topic, but we can use macros to help defining static extensions here. @Show(EdgeInset) // Injects all EdgeInsets static member into EdgeInsetGeometry
static extension on EdgeInsetGeometry {} With macros, boilerplate can be reduced without modifying the language directly. |
WOW, that was quick: 😄 Could someone open an issue to discuss the "injection" mechanism? Is it show/hide, macro or not, what to do about duplicate method names, etc. |
The language team has come to consensus on the following parts from meetings in the past two weeks: Members that can be accessed:
Constructor invocation syntax:
Extent:
Namespace
The points below have not been contested, so I’ll assume we’ll move forward with these. If not, please bring up concerns and further discussion.
Static extensions
And lastly, our open issues are: Syntax:
Equality:
@dart-lang/language-team again to discuss the opens above. |
If I'm missing or misunderstanding anything, feel free to call it out. Thanks. |
Since we allow any selector chains, we do not restrict to "one static access", so there are no requirements on the accesses other than that the result is eventually type-valid. This question only matters for limited syntax, and it's about whether the limit is grammatic or semantic: Is Allowing full selector chains makes the distinction irrelevant. (With "only" selectors, not assignment, one cannot do That leaves equality. If we're comfortable using the static type of the LHS, made nullable, as context type for the RHS (and in a way that does not cause dynamic downcasts, but which does cause downcasts to the parameter type of the It does mean: class Weird {
static const dynamic yes = 42;
static const dynamic bad = "not";
bool operator ==(covariant int x) => x.isEven;
}
void main () {
print(Weird() == .yes); // prints true
print(Weird() == .bad); // throws type error
} The expression after That feels weird. "Then don't do that." Allowing |
Consider a valid program: extension double on num {
static final x = 1.0;
}
void main() {
var a = 1.5;
a = double.x; // using extension name as a namespace
print(a); // works!
} Q: Can we use the shortcut |
A: No. The accidental clash of names has no effect on anything other than making it harder to directly refer to |
I agree with this strongly for I guess they're both OK, but we should be cautious about not painting ourselves into a corner.
I would assume that we allow
I would like this to work, ideally, but I'm not thrilled about special-casing this operation in the language. I have some half-formed ideas for a "soft
I don't think so. For better or worse, operators are not symmetric in Dart (thanks, Smalltalk) and I think it would be weird to try to pretend that isn't the case. |
I agree that if we add real union types, we have issues. At that point we could choose to allow The alternatives here are:
If we compare that to
It so happens that Here we chose the second option because it's enough, currently equivalent to the future-with-general-union-types-safer third option, and it's very annoying if you can't do If/when we get static extensions, I think we should just special-case our already special-cased union types until we actually get general union types, if that ever happens, and then figure out something then. And really just for the case where someone is using a For Future<MyEnum> something() async {
something ... await ... something;
return .value; // Context type `FutureOr<MyEnum>`
} We can try to special case return statements of So, even if it is just for The alternative is to use both/all scopes of a union type from the start, so |
Very nice analysis, @lrhn! I'd prefer to use all scopes implied by a union type, for both union types, with the "conflict means no shorthand" disambiguation. This seems to be pragmatic and useful, reasonably intuitive, and also reasonably future-proof. |
From today's language team meeting, we've decided the following (pulled from the version history of the enum shorthands spec that @lrhn just updated):
|
This is an attempt to summarize the options currently on the table for enum shorthand functionality.
The core functionality is writing
.id
to refer to an enum value (or enum-like value) as on an implicit class derived from the context type.Members that can be accessed
The feature can be extended to allow access to more than just static constant variables.
Different options include:
.id
.id
respectively.id(args)
..id
,.id(args)
and.id<typeArgs?>(args)
respectively.The current consensus seem to be any static getter, constructor or function invocation.
Constructor invocation syntax
.id(args)
, cannot call unnamed constructor..new(args)
for the unnamed constructor..(args)
for the unnamed constructor.Allows consistent use of unnamed constructors, with the syntax we have for treating it as a named constructor.
To invoke a constructor as
const
in a non-const context, prefixconst
:const .id(args)
/const .new(args)
/const .(args)
(Mainly to lower the usability cliff and make being
const
orthogonal to this feature. Changing from.id(args)
toconst LongNameOfClass.id(args)
would feel annoying.)Extent
Other languages allow
.id
to be used in places where it doesn't have a context type, but the expression it starts does..id
/.id(args)
-or-.new(args)
/.id<typeArgs?>(args)
allowed of getter, constructor and function respectively..id
/.id(args)
-or-.new(args)
/.id<typeArgs?>(args)?
allowed, including a.id<typeArgs?>(args)
invoking a function typed getter..('new'|id)(<selector>*)
allowed as shorthand expression, context type used is that of entire selector chain expression. Result must then end up having the correct type.('new'|id) (<selector>*) (assignableSelector op?= expression)?
allowed, matching the extent of null shortening/cascades.<shorthandExpression> <op> <expression>
also allowed, context type used for implicit access of receiver is that of outer expression. (Generally the outermost expression with a leading.id
uses the context type of that.)Namespace
The static namespace that
id
is looked up in is derived from the context type scheme. Not all schemes denote a static namespace, and it's an error if it doesn't.It may be possible for a context to denote multiple namespaces, if so there needs to be a disambiguation logic.
If it's only one namespace, no such logic is needed.
A disambiguation conflict is a compile-time error.
Possibilities in order of increasing
C<T1,...,Tn>
(orC
ifn
is zero) whereC
is a type introduced by aclass
,enum
,mixin
orextension type
declaration denotes the static namespace ofC
.S?
denotes the same namespace asS
.Null
. Conflict if the name is in more than one namespace.FutureOr<S>
denotes the same namespace asS
.Future
. Conflict if the name is in more than one namespace.Plus optionally:
foo(Color x from Colors) ...
introduces a context where a shorthand.red
is looked up in bothColor
andColors
.Colors
.foo(Color x from Colors | MaterialColors | BananaColors)
(strawman syntax!)Instantiated scope for constructors
If the context type is an instantiated type,
C<T1, T2>
, the a constructor invocation.id(args)
is equivalent to either:C.id(args)
(which will hopefully have context typeC<T1, T2>
and will infer<T1, T2>
).C<T1, T2>.id(args)
.NumList<E extends num> extends List<num>
andhas a
fromNums
constructor,List<String> x = .fromNums(1, 2, 3);
won't be valid.C<T1, T2>
toNumList<E>
?Static extension members
Should definitely apply.
If multiple namespaces are available for resolving the shorthand, extension methods should:
Equality (
==
and!=
)An expression of the form
e1 == .id
gives.foo
a context type scheme of_
today. (It should have given it the parameter type ofe1
's static type's==
operator, made nullable.)_
denotes no static namespace.e1 == <shorthandExpression>
(and!=
everywhere we says==
) and use the namespace denoted by the static type ofe1
as target namespace for the shorthand expression. (Wheree1
cannot be a shorthand expression, not unless we include.id op e
above).e1 == condition ? .foo : .bar
.e1 == <shorthandExpression>
and use the static type ofe1
, made nullable, as context type for the RHS.<shorthandExpression> == e2
wheree2
is not a shorthand expression, and symmetrically infere2
first and use its static type, made nullable, as target namespace for the LHS.
@dart-lang/analyzer-team Did I forget anything?
My personal current recommendation is (with the goal of
.id
always being equivalent toFoo.id
for some easily deducibleFoo
):.id
for getters, optionalconst
plus.id(args)
/.new(args)
for constructors, and.id(args)
or.id<typeArgs>(args)
for functions. No calling getters, no tearing off constructors or functions, whether it manages to type-check or not. (If we get implicit constructors, or other coercions, more things might start type-checking!)S?
andFutureOr<S>
denotes the same namespace asS
(but notNull
/Future
), and just the one namespace denoted by a class/enum/mixin/extension type context type. No subtypes, no scope-on-the-side. So.id
is equivalent to someFoo.id
.Foo<int> x = .id(42)
is equivalent toFoo<int> x = Foo.id(42);
which then infers asFoo<int>.id(42)
from context. (It makes a difference if the context type is super-bounded. It may make a difference for static extension constructors with different bounds, if such can exist.)Foo.id
.Context type because it matters for constructors.
==
,whether the RHS is a shorthand expression or not. (Not as a required context type, just as a suggestion.)
==
doesn't have a (covariant
) parameter type that's a proper subtype ofObject
. Which never happens anyway.So grammar:
The text was updated successfully, but these errors were encountered: