Open leafpetersen opened 2 weeks ago
I guess the question of whether the declaration itself is an error is only relevant for the variant in which we use a separate declaration form for static extensions, i.e.:
// Error? Or allowed?
static extension E on D {
static int bar() => 3;
}
since the variant without the leading static
is already valid Dart code.
Should static extensions work when the on type is a typedef?
Yes. It's just another way to write a type.
We have rules for when you can access static members through a type alias. We should use the exact same rules for when you can add static members through an extension.
That is, if we have a class-like type declaration ... Foo<...> {... members...}
and a type alias typedef Bar<...> = Foo<...>;
where the static namespace of Foo
it's the static namespace of Bar
(as I like phrasing it), then an extension on Bar<...>
adds static members to the static namespace of Bar
, and e the static namespace of Foo
.
If so, are they only accessible via the typedef name, or only via the name of the underlying class, or both?
Both, because a normal static member on the target type would be accessible through both. And there are no new static namespaces, so "only the type alias name" is not an option.
And more reasoning why we should allow it.
A available extension declaration of
static extension MyList<T> on List<T> {
List.singleton(T value) : this.filled(1, value);
static List<Object?> parse(String list) => jsonDecode(list) as List;
}
should allow users to use List.singleton
and List.parse
exactly as if they were declared on List
directly.
That includes accessing them through type aliases for List
.
The complications do come with generics, and how we choose to apply those.
For static members (not constructors), which have no access to type parameters of the class,
we should just let the static members be extension members of the static namespace of the on
type.
Here that means parse
can be found when doing a lookup on the static namespace of the List
class definition, and failing to find a member in that namespace. Then we check static extensions on that namespace for alternatives.
For a slightly less trivial example:
static extension NumList<T extends num> on List<T> {
List.singleton(T value) : this.filled(1, value);
static List<num> parse(String source) => (jsonDecode(source) as List).cast<num>().toList();
}
typedef IntList = List<int>;
typedef StringList = List<String>;
Here you can access static members of List
through IntList
, it ignores the type argument to List
for that, but if you access a constructor, it's a constructor for List<int>
, and if you tear off IntList.filled
, it's tearing off List<int>.filled
.
So generics are passed through to the constructor.
The simplest model to keep here is that the static namespace has no type arguments, static extensions adds extension members to the static namespace, and constructors for generic classes take extra type arguments to instantiate the
class, placed before the name instead of after.
It just doesn't work for constructors, because extension constructors refer to the type parameters of the extension, which may be different in number and bounds, than the type being constructed.
And an IntList.singleton
should denote the non-generic function NumList<int>.singleton
, whereas List.singleton
should denote the generic function NumList.singleton
. Instantiation of the receiver type matters.
So we need to do something to go from IntList
/List<int>
/List
to a binding, lack of binding, or even partial binding, of the type parameters of the extension.
The big question is whether we do that before or after deciding applicability. If after, then we have no specializing constructors by type. (But I'd rather do that directly in the constructor declaration as List<T extends num>.name(T value) ...
.)
A failure to find a valid binding is then a compile-time error.
If before, then we can make a failure to find a valid binding mean the extension is not applicable.
We probably do need to do something to ensure a consistency between the written receiver type and the extension on
type.
static extension Bad on List<int> {
List.bad() : this.empty();
}
var list = List<num>.bad(); // Creates a `List<int>`, not `List<num>`. Type-sound, but ...
var list2 = List<String>.bad(); // Should ensure that the result is assignable to `List<String>`.
Here list2
could technically ignore the <String>
and say that List.bad
returns a List<int>
and infer that as the type of list2
. We don't want that. The static type of List<String>.bad()
should be List<String>
. If that's not what it creates, either the constructor should not apply, or it's a compile-time type error.
Then there is:
class BadClass<String> extends List<String> {
BadClass() : super.bad(); // Is really super bad!
}
The super.bad
denotes a generative constructor of "List
", but it's one that can only create a List<int>
.
So we may want to say that an extension generative redirecting constructor is treated as a factory constructor when it comes to super-class constructor invocations. Aka: Don't!
(I keep going in circles when trying to figure this out. It may just be complicated, but it feels like there should be some simpler rule to unify it all.)
Extensions can be defined with typedef names as the on type:
Should static extensions work when the on type is a typedef? If so, are they only accessible via the typedef name, or only via the name of the underlying class, or both?