Skip to content

Should typedefs be usable as classes (when their definition is a class) #89

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

Closed
leafpetersen opened this issue Nov 8, 2018 · 14 comments
Closed

Comments

@leafpetersen
Copy link
Member

This issue is for discussion of the open questions around whether type names (see proposal) introduced by generalized typedefs should be usable in contexts expecting class names when the RHS of the typedef is itself a class type. For example:

class A<T> {
  static int staticMethod() => 3;
  A.named();
  A();
}

typedef MyA = A<int>;
typedef MyGA<T> = A<T>;

class B extends MyA {} // Allowed?
class C implements MyA {} // Allowed?
class D extends Object with MyA {} // Allowed?

void main() {
  // Which (if any) of these should be valid?
  new MyA();
  new MyA.named();
  MyA.staticMethod();

  new MyGA<int>();
  new MyGA<int>.named();
  MyGA.staticMethod();
}
@leafpetersen
Copy link
Member Author

For reference, Kotlin allows all of these.

open class A<T>() {
    companion object {
        fun someStaticMethod() {print("Got it")}
    } 
    constructor(x : Int) :this() {print(x)}
}

typealias MyA = A<Int>;
typealias MyGA<T> = A<T>;

class B : MyA() {}

fun main() {
    MyA();
    MyA(2);
    MyGA<Int>();
    MyGA<Int>(2);
    MyA.someStaticMethod();
    MyGA.someStaticMethod();

@leafpetersen
Copy link
Member Author

cc @eernstg @munificent @lrhn

@yjbanov
Copy link

yjbanov commented Nov 8, 2018

Supporting it would be consistent with the motivation of the feature. If Map<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>, SnackBar> is verbose then the following is still verbose:

typedef ScaffoldMap = Map<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>, SnackBar>;

ScaffoldMap map = Map<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>, SnackBar>();

And if you are not using Flutter style guide and use var instead, then the typedef has no effect in the code above.

OTOH, this would actually make the code concise:

ScaffoldMap map = ScaffoldMap();

Whether you are using types or var.

@munificent
Copy link
Member

Ooh, this is an interesting one.

Here's my ten-minute take:

new MyA();
new MyA.named();

Yes. It felt a little weird to me, but, like Yegor notes, this is like half of the main value proposition of the whole feature.

MyA.staticMethod();

I think we basically have to, yes. It would be profoundly confusing if we didn't since, with optional new, the distinction between static method and constructor is increasingly an implementation detail. Users would be really surprised by:

MyA();              // OK.
MyA.named();        // OK.
MyA.staticMethod(); // Not OK.

It does feel kind of strange because MyA.staticMethod() implicitly ignores the type argument that MyA applies, since static methods doesn't have access to class type parameters. But, in practice, I don't think that will matter much.

new MyGA<int>();
new MyGA<int>.named();

Yup. I think users will expect it. I can see it being practically useful as a way to partially apply type arguments to a generic type:

typedef StringMap<V> = Map<String, V>;

new StringMap<int>(); // Etc.
MyGA.staticMethod();

I think this is fine too.

@eernstg
Copy link
Member

eernstg commented Nov 9, 2018

I consider it to be a questionable idea to allow MyA.staticMethod() to mean A.staticMethod() when we have typedef MyA = A<int>;. The ability to indirectly give type arguments to the class in a static method invocation and then ignore them is an anomaly. It's also going to impede some future generalizations if we insist that these type arguments are ignored when they are provided via a type alias, but they have a semantics when provided like A<int>.myStaticMethodWithAccessToTypeVariables().

But note that the error-prone verbosity that we are fighting occurs in a complex list of type arguments, it doesn't have anything to do with the length of the name of the class vs. the name of the type alias.

Both names would be in the same name space, so if the class name has to be LongAndDescriptiveClassName in order to help developers understand its purpose and avoid namespace pollution then we'd also need LongAndDescriptiveTypeAliasName. So when we consider static member access then we are comparing name1.myStaticMethod(...) against name2.myStaticMethod(...), which doesn't make anything more concise.

@lrhn
Copy link
Member

lrhn commented Nov 9, 2018

I don't think allowing statics on instantiated generic types will block us from later allowing access to those type arguments. Currently it is a compile-time error if a static member refers to a class type variable, so no existing static code can depend on the value of the type arguments (and hence cannot detect whether they are discarded or not).

There is just no other way to actually access a static member than RawType.staticMember (ob-except inside the class). If we add List<int> as a type literal (we should), then there is no syntactic issue with List<int>.copyRange(list1, start1, list2, start2, end). I think it would automatically be allowed by the specification if we don't deliberately prevent it (we extend the notion of "type literal", which is what the static member lookup depends on - or maybe it will just be inconsistent because some text assumes a type literal to denote a class directly).
I fear it may be confusing if you can write List<int>.copyRange but the int doesn't do anything. Then again, you can write a lot of meaningsless things, the solution is usually to catch that in code reviews, not restrict the language

I do worry about use-cases like typedef _ = SomeClassWithStaticMembers; ... _.foo ... _.bar because they hurt readability (even if they are semantically trivial). It's still about abbreviating a long type name.

@leafpetersen
Copy link
Member Author

It is a bit odd, dropping the type arguments. But the counter argument feels really strong to me. If renaming a class Foo to FooNew but leaving the Foo name in place via a typedef is a non-breaking change (up to maybe things that inspect the runtime type), that would be a really nice property. And just in terms of uniformity, having to drop back to the old name to access static method feels like a wart.

@eernstg
Copy link
Member

eernstg commented Nov 12, 2018

Taking a look at Swift type aliases, they can be non-generic and generic just like Dart's, but missing type arguments are taken to be an abbreviation for passing all type arguments as-is (so typealias Diccionario = Dictionary is what we would write like typedef Mapa<K,V> = Map<K,V>).

I haven't yet found any evidence to confirm or reject the assumption that it's possible to use a type alias that denotes a parameterized type for accessing static members (and it seems to take a number of steps to get started using Swift).

@eernstg
Copy link
Member

eernstg commented Nov 12, 2018

About the actual topic of this thread, I agree that we get pretty good coverage if we allow the raw type alias name to stand in for the raw class name in a static member access.

That could be because we i2b the actual type arguments and then ignore them, or it could be because we recognize that situation and allow the type alias to denote the namespacy usage of the class name. Note that the latter interpretation implies that we do not ignore the type arguments, we just allow OldName to be used for static access as well as NewName, and we never have any type arguments, inferred or explicit:

class NewName<X, Y> {
  ...
  void staticMethod() {...}
}

typedef OldName<X, Y> = NewName<X, Y>;

main() {
  OldName.staticMethod();
}

This approach might be less of an anomaly. It would fit the use case where a class is renamed, but we do not change it from non-generic to generic. In contrast we would need to allow the actual type arguments to be ignored if we also want to support the following:

// BEFORE.

class OldName {
  ...
  void staticMethod() {...}
}

main() {
  OldName.staticMethod();
}

// AFTER.

class NewName<New, Type, Arguments> {
  ...
  void staticMethod() {...}
}

typedef OldName = NewName<WithSome, Default, TypeArguments>;

main() {
  OldName.staticMethod(); // Error, unless we can ignore type arguments.
}

So we may be able to keep the anomaly small if we only support static accesses for type aliases which (1) have no type parameters and pass no type arguments, or (2) have a type parameter list and passes it on to the type on the right hand side. The only nasty case is the hybrid where we have a non-generic type alias that passes some type arguments on to a generic class on the right hand side (explicitly or via i2b).

But there could also be other situations worth considering, e.g.:

class C<X, Y, Z> {
  ...
  void staticMethod() {...}
}

typedef NormalC = C<String, int, Map<String, int>>;

main() {
  NormalC.staticMethod();
}

Now consider the situation where we decide that NormalC is such an important special case that we want to create a class for that and optimize it for that particular case:

class NormalC implements C<String, int, Map<String, int>> {...}

This will break NormalC.staticMethod() because NormalC isn't the class C any more. So we should expect some breakage if we reinvent certain commonly used type aliases as new classes.

@lrhn
Copy link
Member

lrhn commented Nov 12, 2018

Swift allows statics on generics classes with access to the type argument. They do not allow access through the raw type without inferring a type. They do allow access through type aliases.
And they do not allow static fields on generic classes at all:

main.swift:6:14: error: static stored properties not supported in generic types
  static var x = 42;
  ~~~~~~     ^

(using http://online.swiftplayground.run/).

@eernstg
Copy link
Member

eernstg commented Nov 12, 2018

In other words, Swift considers a static member to be a member of the instantiated generic class ("that has received its type arguments already"), except that they don't want to handle a separate copy of the state for each actual type argument list.

So even though that seems like a nice and coherent starting point, they also managed to make static members a bit messy. Surprise surprise. ;-)

@leafpetersen
Copy link
Member Author

Just to make @lrhn's summary above concrete, example swift code:

class A<T> {
    static func ss() {}
    class func cc() {}
    // Not allowed in a generic static var v = ""
}

typealias B<T> = A<T>;
typealias C = A<Int>;

func test() {
    // Not allowed A.ss;
    // Not allowed A.cc;
    A<Int>.ss();
    A<Int>.cc();
    B<Int>.ss();
    B<Int>.cc();
    C.ss();
    C.cc();
}

@eernstg
Copy link
Member

eernstg commented Nov 26, 2018

CL 81414 was updated such that the rules are maximally permissive (in particular, it is allowed to invoke static methods using expressions like F.myStaticMethod(), even in the case where F denotes a non-generic type alias whose right hand side is a parameterized type C<Some, Type, Arguments>, in which case it means C.myStaticMethod()).

@eernstg
Copy link
Member

eernstg commented Jan 14, 2020

Closing: We are using the maximally permissive approach specified by CL 81414.

@eernstg eernstg closed this as completed Jan 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants