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
Class methods - static methods with access to generic type parameters. #3781
Comments
class C<T> {
static List<inherited T> single(T value) => <T>[value];
} The idea is that once the method is static, it's better to be spelled out like a static method. It's a treatment of T that's different. |
Just curious as to why we can't extend static methods to have access to the classes type parameters? If there is a conflict, the classes parameters would be shadowed by the type parameters on the static method itself, so references to existing type parameters would be unchanged. |
We could just change all static methods to be able to refer to the type parameters. It can't be allowed to work for variables, but that's an easy restriction. Just don't put the type variables in scope. It would mean that a static method may or may not refer to the type variables. If the function doesn't use the type parameter, then it doesn't need to close over it. But we don't want to rely on that. Could work. Is simpler. |
One (potential) problem is about restrictions on type parameters. Example: somebody declared the class like this: class Foo<T extends Widget> {
static List<T> bar(T value) => <T>[value];
} T in the definition of "bar" has no restrictions, so this T has nothing to do with T in the class definition. Why is it called T then? Probably because people habitually write T for the type parameter. Now suppose we allow a new invocation syntax. Can we write The second problem is related to the first one. Consider a variant: class Foo<T extends Widget> {
static List<E extends Widget> bar(E value) => <E>[value];
} Before the proposed change, it didn't matter whether we write a type parameter of |
If this is using the syntax above, without the If we had a syntax where the static method declares the parameter that can be applied along with the class name, it'd probably be on the class Foo<T extends Widget> {
static<T> List<T> bar(T value) =><T>[value];
// ^^^ declaration
// ^ ^ ^ references
} then there is no question that class Foo<T> {
static<K, V> Map<K, Foo<V>> cache() => <K, Foo<V>>{};
} That can be confusing. I would just not do that, precisely because it's confusing. Either use the class type parameters, or use a generic function (or both), but this is not trying to introduce a third kind of type parameters. The idea is precisely to give access to the type parameters of the surrounding declaration, which means it would just be: class Foo<T extends Widget> {
// ^ declaration
static List<T> bar(T value) =><T>[value];
// ^ ^ ^ references
} and the The underlying problem that I'm trying to solve is to reduce the API distinction between constructors and static functions. Today you can do: List<int>.filled(42, 42) but you cannot do: List<int>.fromStream(someIntegerStream) and have it return a List.fromStream<int>(someIntegerStream) which is OK, but the difference is slightly grating. By allowing |
Sorry, my mistake. Currently, if you write class Foo<T extends Widget> {
static List<T> bar(T value) =><T>[value];
} you will get an error "static members can't reference type parameters of the class". class Foo<T extends Widget> {
static List<T> bar<T>(T value) =><T>[value];
} but T in class Foo<T extends Widget> {
static List<T> bar<T>(T value) =><T>[value]; // T can be whatever, e.g. int
static<T> List<T> bar1(T value) =><T>[value]; // T references an "inherited" T, which MUST extend Widget
} The difference is that you will now be able to pass the type parameter to (Edited, replacing one sort of confusion with another :-) |
Right! And that's why I don't like the So instead, if we go with class C<T extends num> {
static T foo1<T>(T value) => value;
static T foo2(T value) => value;
T foo3<T>(T value) => value;
T foo4(T value) => value;
} Here There is nothing fundamentally new here, not much at least. We just allow static members to access the type parameters just like constructors already can, if they want to and haven't shadowed the name. No existing code is hurt, we don't change identifier resolution, we just allow something that was previously forbidden. (And give it a meaning.) And reading whether a function depends on the type parameter is no more or less difficult than it is for instance members already. |
Oh, I see! By omitting the C<int>.foo2(1); // correct: T propagates from C<int>
C<int>.foo1(1); // error!
C.foo2<int>(1); // error or not?
C.foo1<int>(1); // correct
C.foo2(1); // error or not? does inference work? Does it work in extensions? (Please fix a small typo: "The There's one (theoretical) case we haven't yet discussed: |
I don't think that's a problem, it would be declared like this: class C<T> {
static (T, E) foo5<E>(T a, E b) => (a, b);
} No need for a new syntax, just don't shadow the class type parameters. Invocations would then look like this: |
And what if class C has more type parameters? class C<T, V> { // V is not used by foo5
static (T, E) foo5<E>(T a, E b) => (a, b);
} Then the invocation will look like |
This is already analogous to the situation today where a function declares type parameters it does not use: T bar<T, E>(T value) => value; This needs to be invoked as As you say though, hopefully this won't arise too often thanks to type inference. |
Suppose in the body of the extension method I want to call another static method from the same or different extension. extension <T extends num> on List<T> {
static foo(T t) {
// I want to call another static method from the same or different extension
List<T>.bar(t); // will it work?
}
static bar(T t) {
//...
}
} |
What is suggest: // Valid. T of C bound to int, used in foo2
C<int>.foo2(1);
// Valid. Doesn't use T of C.
// T of foo1 inferred as int.
C<int>.foo1(1);
// Error, foo2 is not generic
C.foo2<int>(1);
// Valid. Just like today.
// C instantiated to bound, never used
C.foo1<int>(1);
// Valid. C.foo2 infers as C<int>.foo2(1)
C.foo2(1); About extensions: it will work if we get extension static methods, otherwise the static is only on the extension itself. |
Static methods cannot access type variables of the surrounding class/mixin/enum/extension type (or extension).
I propose introducing class members which can.
Proposal
Syntax
Allow prefixing a member declaration with
class
instead ofstatic
to make it a class member.Variable declarations cannot be class members.
Extension declarations are not allowed to be
class
members.A
class
,enum
,mixin
orextension type
member is.(Maybe we can allow extension members too.)
Class members are scoped like static members, and conflict in the same ways. The only difference is that you can invoke a class member like you invoke a constructor, with type arguments on the type.
That is
C<T>.single(42)
is valid syntax. It already is, it can just now be used for other declarations than constructors.Class methods can be generic functions.
If a declaration could be
static
, and it's not a variable declaration, it can beclass
instead.Static semantics
The body of a class member can reference the type parameters of the surrounding class (or similar).
Invoking
C.mapIt(a, b)
, with a raw receiver, infers type arguments toC
if possible. It is treated as ifC<_>.mapIt
is a single method declaration with two type parameters, and those type parameters are inferred as we normally would for a single method.If no context or arguments are available to infer the type parameters from, they are instantiated to bounds.
We can also infer type arguments to the class for static setters and getters, which may be new, since those have so far not been able to be generic.
An unqualified identifier which resolves to a class member is inferred in the same way as a raw identifier,
except that if there is no context to infer type arguments to, and the invocation is from an instance or class member,
it uses the current binding of the type parameters instead of instantiating to bounds.
If invoking from a static method, it works just as if invoking with an explicit raw type.
This is consistent with how other unqualified identifiers reference members of "the same" surrounding construct (fx extension methods).
Invoking a class member through a type alias works just as invoking a static member (only works when the alias directly denotes a type declaration), except that the class member can access type arguments to the type.
A class method tear-off is a constant expression if the type arguments to the class are constant, whether explicit or inferred. (The tear-off will close over those type arguments.)
A class member of a non-generic class is indistinguishable from a static member.
Semantics
The scope that the class member is evaluated in contains type variables corresponding to the surrounding class,
just like for a constructor.
(Unlike an instance member, the type variables are not read from a
this
instance, and there is nothis
in scope.)Class methods can be torn off. If they are, they close over the type variables of the class they're torn off from.
The runtime type of the torn-off function is the function type of the member that was torn off.
It's not possible to abstract over the instantiation of the class, tearing a method off a "raw type".
Considerations
The
C<int>.single(42)
looks like a constructor call, but isn't.But then
C.single(42)
looks like a constructor call too, and that can be just a plain static function on a non-generic class.We've relied on naming to distinguish constructors and static functions (and allowed static factory functions to use constructor-like names when it makes sense), so I don't see any new issue here.
There should be no ambiguity with syntax. The reserved word
class
cannot be used inside class-like declaration bodies, so it's new. It's placed in the same place asstatic
.Invocation syntax is not new, it's already used by constructors. We can now do
C<int>.foo = 42
, which is new, but not grammatically challenging. Since it's only static-like declarations, there are no operators.If we ever plan to have nested classes then we may have a problem. I'm ready to consider a different name for
class
member, or wait until it becomes necessary.If we get extension static methods (#723), then we should probably also allow extension class methods.
(Will have to consider whether access to type variables is ambiguous - how do we go from the type variable binding of the target class to the type variable binding of the extension? Probably the same way we always do.)
The text was updated successfully, but these errors were encountered: