dart-lang / language

Design of the Dart language
Other
2.68k stars 205 forks source link

[Static extensions] Should we allow static extensions to be accessed on any named type? #4055

Open leafpetersen opened 3 months ago

leafpetersen commented 3 months ago

The current proposal (#3835) for static extensions only allows static members and constructors to be accessed only on "class-like" types (that is classes, mixin classes, mixins, enums, and extension types). In principle, we could relax this restriction, either in the case that the on type is a typedef (see #4052) or in the case that the receiver is a typedef name. For example, we could allow:

typedef Pair<T> = (T, T);

extension E<T> on Pair<T> {
   factory Pair.twice(T x) => (x, x);
}
void test() {
   var (x, y) = Pair.twice(3);
   assert(3 == y);
   assert(3 == x);
}

If so, do we attach the extension to the name Pair or to the underlying type? That is, does the following work?

typedef Pair<T> = (T, T);

extension E<T> on (T, T) {
   factory Pair.twice(T x) => (x, x);
}
void test() {
   var (x, y) = Pair.twice(3);
   assert(3 == y);
   assert(3 == x);
}

cc @dart-lang/language-team

lrhn commented 3 months ago

No. I'd only allow adding extension static members to existing static namespaces.

If we say "any type can have static members", we'll have to explain why List<int> and List<String> do not have different static namespaces, but List<int> Function() and List<String> Function () do. (Or say how to collapse those function type namespaces into one.)

The static namespaces are introduced by class, mixin, enum, extension type and extension declarations. There are no other static namespaces than those.

Some type aliases have a shape which we say denotes a static namespace, and we let them also alias that existing static namespace. (Which cannot be an extension namespace, since those do not introduce a type.)

With this feature, some extensions will also act as extensions of the static namespaces of their on type, if the on type clause has a shape that denotes a static namespace. When we would otherwise do a static namespace lookup on that namespace, and we find no member with the needed name, we check if any extension extends that static namespace with something of the same base name, and if a single one is applicable, the static reference denotes that static member definition.

(And if we did, we would not attach anything to type alias names.)


To be a little more formal:

An identifier or qualified identifier T denotes a static namespace, N, if and only if:

A type clause T, which must denote a type, also denotes a static namespace, N, if and only if:

A static access on T, where T is an identifier or qualified identifier which denotes a static namespace, has one of the forms:

In each case, the static namespace is searched for members with base name id, or for an unnamed constructor for the new or no-name (constructor-only) cases. If one is found, that is the member denoted by the T.id/T.new/T (as constructor).

With static extensions, let's go with the variant without static, a declaration of:

extension NumList<T extends num> on List<T> {
  factory List.sorted(Iterable<T> values) => [...values]..sort();
}

will be a static extension on the static namespace denoted by List<T>, if any. (In this case, the static namespace introduced by the class List declaration).

When doing type inference for a static member access like var x = List.sorted([3, 2, 1]);: