dart-lang / language

Design of the Dart language
Other
2.61k stars 200 forks source link

Abstract static methods #356

Open HugoKempfer opened 5 years ago

HugoKempfer commented 5 years ago

Hi, trying to produce some generic code, I discovered that interfaces can't have non-implemented static methods.

But it would be a nice feature to allow this.

I can illustrate it by this piece of Rust code:

struct A {
    damn: i32
}

trait Serializable {
    fn from_integer(nb: i32) -> Self;
}

impl Serializable for A {
    fn from_integer(nb: i32) -> Self {
        A {
            damn: nb
        }
    }
}

fn bar<T: Serializable>(nb: i32) -> T {
    T::from_integer(nb)
}

pub fn main() {
    let wow = bar::<A>(10);
    println!("{}", wow.damn);
}

I tried to produce a non-working equivalent in Dart:

abstract class Serializable {
  static fromInteger(int);
}

class A implements Serializable {
  int foo;

  A(this.foo);

  A fromInteger(int nb) {
   return A(nb);
  }
}

T bar<T extends Serializable>(int nb) {
    return T.fromInteger(nb);
}

main() {
    var wow = bar<A>(42);

    print(wow.foo);
}
MarvinHannott commented 5 years ago

Shouldn't fromInteger rather be a constructor? That would actually work. But yes, it might be a good idea since Java interfaces can do that.

HugoKempfer commented 5 years ago

Shouldn't fromInteger rather be a constructor? That would actually work. But yes, it might be a good idea since Java interfaces can do that.

In my comprehension, it won't work since the goal is to instantiate any class that implements Serializable. If fromInteger is implemented as a constructor of A, it's still impossible to call it from T generic type.

MarvinHannott commented 5 years ago

In my comprehension, it won't work since the goal is to instantiate any class that implements Serializable. If fromInteger is implemented as a constructor of A, it's still impossible to call it from T generic type.

You are right, my fault. But I think I know understand why it doesn't work: If you print the type parameter it will tell you that it were of the type you specified. However, when you print the field runtimeType it will tell you that it actually is the class Type, meaning it is a sham.

eernstg commented 5 years ago

Cf. an old SDK issue on a similar topic: https://github.com/dart-lang/sdk/issues/10667 (search for 'virtual static' to see some connections).

This doesn't fit well in Dart. The main point would be that Rust has a different approach to subtyping,

Subtyping in Rust is very restricted and occurs only due to variance with respect to lifetimes and between types with higher ranked lifetimes. If we were to erase lifetimes from types then the only subtyping would be due to type equality. ... Higher-ranked function pointers and trait objects have another subtype relation.

as stated here.

@HugoKempfer wrote:

If fromInteger is implemented as a constructor of A, it's still impossible to call it from T generic type.

Right; even if A has a constructor named A.fromInteger and the value of T is A, T.fromInteger(nb) will not invoke that constructor. Similarly, even if A contains a static method named fromInteger and the value of T is A, T.fromInteger(nb) won't call that static method.

In general, constructor invocations and static method invocations are resolved statically, and any attempt to invoke them with an instance of Type as the receiver (as in T.fromInteger(nb)) will proceed by evaluating T (which yields an instance of Type that reifies the given type), and then accessing the specified member as an instance member of that Type instance. But Type does not declare an instance member named fromInteger. So T.fromInteger(nb) is a compile-time error, and (T as dynamic).fromInteger(nb) will fail at run time.

You might say that it "should work", and we did have a proposal for adding such a feature to Dart for quite a while, with some preparation for it in the language specification. But every trace of that has been eliminated from the spec today.

The issue is that, in Dart, this feature conflicts with static type safety: There is no notion of a subtype relationship between the static members and constructors of any given class type and those of a subtype thereof:

class A {
  A(int i);
  static void foo() {}
}

class B implements A {
  B();
}

void bar<X extends A>() {
  var x = X(42); // Fails if `X` is `B`: no constructor of `B` accepts an int.
  X.foo(); // Fails if `X` is `B`: there is no static method `B.foo`.
}

It would be a hugely breaking change to require every Dart class to implement constructors with the same signature as that of all of its supertypes, and similarly for static methods. Already the signature conflicts could be difficult to handle:

class A { A([int i]); }
class B { B({String s = "Hello!"}); }

class C implements A, B {
  // Cannot declare a constructor which will accept an optional positional `int`,
  // and also accepts a named `String` argument.
  C(... ? ...);
}

Apart from conflicts, it's not desirable. For instance, Object has a constructor taking no arguments, so all classes would have to have a constructor that takes no arguments, which may not always be useful. This problem gets a lot worse as soon as we consider any other class C than Object, because C will impose further requirements on all its subtypes.

The situation is quite different in Rust. I'm no Rust expert, but we do have the following salient points: The declaration of from_integer in trait Serializable is effectively a declaration of a member of a "static interface" associated with the trait (and hence with all implementations of that trait), because it does not accept a receiver argument (like self or &self). This means that every implementation of the trait must also implement such a function, and we'd use the :: operator to disambiguate the implementation of the trait, and that's allowed to be a type variable.

So we wouldn't want to add anything to Dart which is directly modeled on the ability in Rust to require that all subtypes have a static interface that satisfies the usual override rules.

But it's worth noting that this "static interface" of a Rust trait is similar to the instance members of a separate object, somewhat like the companion objects of classes in Scala, but with a subtype relationship that mirrors the associated classes.

We can emulate that as follows:

abstract class Serializable {
  static const Map<Type, SerializableCompanion> _companion = {
    Serializable: const SerializableCompanion(),
    A: const ACompanion(),
  };

  static SerializableCompanion companion<X extends Serializable>() =>
      _companion[X];
}

class SerializableCompanion {
  const SerializableCompanion();
  Serializable fromInteger(int i) => throw "AbstractInstantionError";
}

class A implements Serializable {
  int foo;
  A(this.foo);
}

class ACompanion implements SerializableCompanion {
  const ACompanion();
  A fromInteger(int i) => A(i);
}

T bar<T extends Serializable>(int nb) {
  return Serializable.companion<T>().fromInteger(nb);
}

main() {
  var wow = bar<A>(42);
  print(wow.foo);
}

We have to write Serializable.companion<T>() rather than T when we call fromInteger, but otherwise the emulation is rather faithful:

The _companion map delivers an object of type SerializableCompanion, so there's nothing unsafe about the invocation of fromInteger. We don't have a guarantee that the returned object is of type T, this is an invariant which is ensured by the choice of keys and values in _companion, and that's not a property that the static typing can detect (but we do know statically that the invocation of fromInteger returns a Serializable). So there's a dynamic type check at the return in bar (with --no-dynamic-casts we'd add as T at the end, otherwise we get it implicitly).

Another issue is that SerializableCompanion.fromInteger throws, which makes sense because we cannot create an instance of Serializable. In Rust we get 'the size for values of type dyn Serializable cannot be known at compilation time' and 'the trait Serializable cannot be made into an object' (and more) if we try to use Serializable as an actual type argument:

...

pub fn main() {
    let wow = bar::<Serializable>(10);
    ...
}

This illustrates that the invocation in Rust is actually quite similar to the one in the above Dart emulation, because it will provide the actual trait object to bar, and that object must have a fromInteger method.

We could turn this emulation into a language design proposal for Dart, although it isn't trivial. Apart from the syntactic noise (that we may or may not choose to reduce by means of some desugaring), the essential missing feature is a special kind of dependent type that would allow us to know that the _companion map is a Map<t: Type, SerializableCompanion<t>>, that is: Each key/value pair is such that the key as a Type, and that type is a reification of a certain type t, and then the value is a SerializableCompanion<t>, with the following adjustment:

class SerializableCompanion<X extends Serializable> {
  const SerializableCompanion();
  X fromInteger(int i) => ... // Ignore the body, the important point is the return type.
}

In the emulation we also need to thread that type argument around, e.g., companion would return a SerializableCompanion<X>, etc. With --no-implicit-casts we get two casts, due to the fact that the static types do not take the above-mentioned invariant into account.

We wouldn't want to add these dependent types to the Dart type system as such, but it is a line of thinking that we could apply when deciding on how to understand a particular syntax for doing this, and also in the implementation of the static analysis. In particular, any data structures similar to the _companion map would be compiler-generated, and it's not so hard to ensure that it's generated in a way that satisfies this property.

So there's a non-trivial amount of work to do in order to create such a feature as a language construct, but the emulation might also be useful in its own right.

eernstg commented 5 years ago

@MarvinHannott wrote:

If you print the type parameter it will tell you that it were of the type you specified. However, when you print the field runtimeType it will tell you that it actually is the class Type, meaning it is a sham.

You do get those outcomes, but it's not a sham. ;-)

When T is evaluated as an expression the result is a reified representation of the type denoted by T. Reified type representations have dynamic type Type (or some private subtype of that, we don't promise exactly Type).

When you do print(T) you'd get the result from toString() on that instance of Type, which might be "A". This means that this instance of Type represents the type A, not that it 'is of' type A (that's a different thing, e.g., new A() is of type A). When you do print(T.runtimeType) it prints Type, because T is an instance of Type.

There's nothing inconsistent about this, and other languages will do similar things. E.g., if you have an instance t of Class<T> in Java and print it then it will print something like the name of the class that it represents, but t.getClass().toString() will be something like 'Class':

public class Main {
    public static void main(String[] args) {
      Class<Main> c = Main.class;
      System.out.println(c); // 'class Main'.
      System.out.println(c.getClass()); // 'class java.lang.Class'.
    }
}
munificent commented 5 years ago

It would be a hugely breaking change to require every Dart class to implement constructors with the same signature as that of all of its supertypes, and similarly for static methods.

The subtype relation in the metaclasses (i.e. static members) wouldn't necessarily have to mirror the class's own subtyping, and I think there are good arguments that it should not. In particular, that follows Dart's current behavior where static members and constructors are not inherited.

To get polymorphism for static members, you could have explicit extends and implements clauses and those could be completely independent of the class's own clauses:

class A {
  a() { print("A.a"); }
}

class MA {
  ma() { print("MA.ma()"); }
}

class B extends A static extends MA {
  b() { print("B.b");
}

class C implements A static implements MA {
  a() { print("C.a()"); }
  static ma() { print("C.ma()"); }
}

test<T extends A static extends MA>(T value) {
  value.a(); // OK.
  T.ma(); // OK. We know the "static interface" of T has ma().
}

test<B>(B());
test<C>(C());

I don't know if this actually hangs together, but back when Gilad was working on the metaclass stuff, I felt like there was something there.

eernstg commented 5 years ago

@munificent wrote:

The subtype relation in the metaclasses (i.e. static members) wouldn't necessarily have to mirror the class's own subtyping,

Right, I remember that we agreed on that already several years ago. ;-)

But if there is no connection between those two subtype relationships then we can't make any assumptions about S having any of the known static members that we know T has, if all we know is S <: T. Consider the example again, specifically bar:

T bar<T extends Serializable>(int nb) {
    return T.fromInteger(nb);
}

We do know that Serializable.fromInteger exists and has a signature that matches the call, but there would be no reason to assume that the actual value of T also has such a fromInteger. So T.fromInteger(nb) is no safer than a completely dynamic invocation.

In this comment I tried to move a bit closer to something which would actually preserve the connection between the two subtype relationships (such that S <: T actually implies that S has static members which are correct overrides of those of T), but only when declared as such, and only for "small sets of classes", such that it would be OK to require some or all parts of the static interface to be supported for all subtypes (because there would only be a few of them). I don't have a complete model for how to do that, but I think it's a direction worth exploring.

munificent commented 5 years ago

But if there is no connection between those two subtype relationships then we can't make any assumptions about S having any of the known static members that we know T has, if all we know is S <: T.

That's why in my example I wrote a static extends type bound:

test<T extends A static extends MA>(T value) { // <--
  value.a(); // OK.
  T.ma(); // OK. We know the "static interface" of T has ma().
}

That's the part where you define the type that the type argument's metaclass must be a subtype of.

eernstg commented 5 years ago

That's why in my example I wrote a static extends type bound:

OK, that makes sense! It would make T harder to provide (any caller that passes on a type variable U that it declares would have to require U extends SomeSubtypeOfA static extends SomeSubtypeOfMA), but I think it should work.

munificent commented 5 years ago

It would make T harder to provide

That's exactly right. Because now you are "doing more" with T, so the constraints placed upon it are more stringent.

rodion-m commented 4 years ago

It'll really useful feature for serializable classes.

AirborneEagle commented 4 years ago

I think I am in the wrong place here, but I cannot find what I am looking for and this seems to be the closest place. I also tend to have a hard time understanding the intricacies of programming language architectures, so please forgive me if this is a simple misunderstanding on my part.

What about static fields in an abstract class?

If I want every one of my repos to have a static String called collectionName, Is there any way to have the abstract BaseRepo enforce that? Something Like this suedo code:

abstract class BaseRepo {
 static String collectionName;
}

class ItemRepo implements BaseRepo{
  static String collectionName =  'items';
}

I also would like some way to enforce that every impl. or inheriting class is a singleton. but I have not yet found a way to force that by way of inheritance. Like this:

abstract class BaseRepo<T> {
 static T instance;
}

class ItemRepo implements BaseRepo{
  static ItemRepo instance =  ItemRepo._internal();
  ItemRepo._internal();
}

maybe there is a way to use mix ins, or extensions? idk. this is what I am going for, but I haven't found a way to make them happen.

asjqkkkk commented 4 years ago

Now it's 2020,Is there any progress?

Manuelbaun commented 4 years ago

Hi,

I also came across the need of a generic type T for a function that needs to be changeable. since you can't do things like:

abstract class Base {
  factory Base.fromMap(Map<String, dynamic> map) => null;
  Map<String, dynamic> toMap() => null;
}

class A<T extends Base> {
  T b;

  createB(map) {
    b = T.fromMap(map);
  }
}

I use a workaround too lookup the right type at runtime and use fromMap there.

abstract class Base {
  factory Base.fromMap(Map<String, dynamic> map) => null;
  Map<String, dynamic> toMap() => null;
}

class Response<T extends Base> {
  String id;
  T result;

  Response({this.id, this.result});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'result': result.toMap(),
    };
  }

  static Response<T> fromMap<T extends Base>(Map<String, dynamic> map) {
    if (map == null) return null;

    return Response(
      id: map['id'],
      result: mappingT2Class(T, (map['result'])),
    );
  }

  @override
  String toString() {
    return 'id: $id; result: ${result.toString()}';
  }
}

dynamic mappingT2Class(t, map) {
  Type myType = t;

  switch (myType) {
    case BaseChild:
      return BaseChild.fromMap(map);
  }
}

class BaseChild implements Base {
  final String id;
  BaseChild(this.id);

  @override
  Map<String, dynamic> toMap() {
    return {'id': id};
  }

  @override
  factory BaseChild.fromMap(Map<String, dynamic> map) {
    return BaseChild(map['id']);
  }

  @override
  String toString() {
    return 'id: $id';
  }
}

It works ok, but I have to manually add the type I like to use to that function mappingT2Class

Peng-Qian commented 4 years ago

Hi,

Will dart improve in this area?

I think annotation could really help.

Please also consider adding @childrenOverride, which means the direct child should override the abstract method even it is an abstract class, this can deeply benefit generated code, such as built_value.

abstract class Dto {
  @factory
  Dto fromJson(Map<String, dynamic> json);

  @childrenOverride
  Map<String, dynamic> toJson();

  @static
  bool isValid(Map<String, dynamic> json);
}
munificent commented 4 years ago

Sorry for the very long delay.

If I want every one of my repos to have a static String called collectionName, Is there any way to have the abstract BaseRepo enforce that? Something Like this psuedo code:

abstract class BaseRepo {
 static String collectionName;
}

class ItemRepo implements BaseRepo{
  static String collectionName =  'items';
}

I also would like some way to enforce that every impl. or inheriting class is a singleton. but I have not yet found a way to force that by way of inheritance.

Why do you want to enforce that? What does that enable you to do? Since static methods are not polymorphic, even if you require a handful of classes to all have the same static method, that doesn't actually give you any new affordances.

danielleiszen commented 3 years ago

Please see the following arrangement:

abstract class Converter<T> {
  T convert(String source);
}

class Convertable<T> {
  static Converter<T> converter();
}

class Loader<T extends Convertable<T>> {
  T loadFromString(String source) {
    var converter = T.converter();

    return converter.convert(source);
  }
}

class Data with Convertable<Data> {
  @override
  static Converter<Data> converter() {
    return DataConverter(); // specific converter instance
  }
}

Is this possible to acquire this result without abstract static methods? Thank you.

gisborne commented 3 years ago

I also would like this. Let me offer a concrete use case: namespaces that support nesting and differ in storage methods (in-memory; local sqlite; remote api; …). So they all have the same methods (get/set with paths).

To implement set, I would like to do something like this in an abstract superclass. Assume I can access the class of the current object using .class and use that as the default constructor:

abstract class Namespace {
  final String address;

  Namespace(): address = Uuid();

  void set({List<String> address, String it}) {
    if (address.length > 1) {
      _set_with_string(address: address[0], it: it
    } else {
      _set_with_string(address: address[0], it: this.class(address: address.sublist(1), it: it).address, nested: true)
    }
  }

  void _set_with_string({String address, String it, Boolean: nested = false});
}

This is a fine design, but afaict, not currently doable in Dart. The best workaround I can think of is to have a new_instance method in each subclass that invokes the constructor. Which isn't horrible, but certainly counts as a design smell caused by a language issue.

esDotDev commented 3 years ago

Why do you want to enforce that? What does that enable you to do? Since static methods are not polymorphic, even if you require a handful of classes to all have the same static method, that doesn't actually give you any new affordances.

We have a similar use case with Page routes in flutter. What I want is this:

// Enforce both a static and instance method
abstract class AppLink {
  static String get path;
  void setArgs(Map<String, String> args); 
}

So implementations need to to provide this name:

class HomeLink extends AppLink {
  static String get path => "/home";
  static void setArgs(Map<String, String> args){}
}

This can be checked later in onGenerateRoute, so we can create the proper page:

 onGenerateRoute: (RouteSettings route) {
    AppLink  link = SplashLink();
    if(route.name == HomeLink.path){
       link = HomeLink()..setArgs(route.args);
    }
    if(route.name == EditorLink.path){
       link = EditorLink()..setArgs(route.args);
    }
    // etc
    return link.buildPage();
}

Removing the static method from inside the class here just causes problems, mainly it makes refactoring harder, since you now need to rename 2 code instances HomeLinkPath and HomeLink, instead of just HomeLink, also it makes organization harder if multiple classes share the same file, since it's easy for the static fields and their associated class to get separated in the file.

munificent commented 3 years ago

In your example, @esDotDev, I don't see what value an abstract static declaration provides. You're calling HomeLink.path directly, so you'll get an error if it isn't defined there anyway.

esDotDev commented 3 years ago

It's about declaring a contract, like any interface is meant to. Our team can have a convention, when you create a new link you must extend AppLink, then the compiler will inform them of methods they need to provide, and as our codebase morphs or changes, will inform them of any new methods that need to be provided.

This is no different than non-static inheritance, you could make the same argument there: what's the point of an abstract method, when you will just get an error if you try and use said method when it is called directly on the concrete class. This is not much different than saying every StatelessWidget needs a build() method, every PageLink, needs a getPath method, maybe fromArgs maybe some other methods down the road. Polymorphism isn't the only use of abstract methods and classes, often its simply about maintaining order, and enforcing structure easily across team members.

The core value is that the code is self documenting, and we're leaning on the compiler to lower devs cognitive load. They don't need to remember all the methods AppLink's are supposed to implement they can just look at / extend the base class, and the compiler will offer them code-completion for all required methods. If we add a new abstract property to AppLink in the future, the compiler lights up, and shows us everywhere that needs fixing (whether those methods are currently called or not), imagine I have some pages, currently commented out, so they are not "in use", when using an interface, those classes will also get flagged for a refactor, instead of only lighting up later when those pages are used.

rrousselGit commented 3 years ago

Won't this be covered by static type extension?

type extension<T extends InheritedWidget> on T {
  static T of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<T>();
  }
}

class MyInheritedWidget extends InheritedWidget {

}

MyInheritedWidget.of(context);
gisborne commented 3 years ago

I’m usually a Ruby dev. I was thinking in terms of classes as objects — if I pass a class into somewhere, I want to specify its behavior. But I don’t believe classes are objects in Dart. Bummer.

On Apr 21, 2021, at 2:53 , Remi Rousselet @.***> wrote:

Won't this be covered by static type extension?

type extension on T { static T of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } }

class MyInheritedWidget extends InheritedWidget {

}

MyInheritedWidget.of(context); — You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/dart-lang/language/issues/356#issuecomment-823935167, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAG7X7F3ZOVQ7G6ZQNIHFDTJ2OBNANCNFSM4HNNAFZA.

danielleiszen commented 3 years ago

Won't this be covered by static type extension?

Type extensions would not enforce the implementation of a specific method or property as @esDotDev pointed.

munificent commented 3 years ago

It's about declaring a contract, like any interface is meant to. Our team can have a convention, when you create a new link you must extend AppLink, then the compiler will inform them of methods they need to provide, and as our codebase morphs or changes, will inform them of any new methods that need to be provided.

It will do that already, though. Because in your example, they will also have to add a new section like:

    if(route.name == SomeNewLink.path){
       link = SomeNewLink()..setArgs(route.args);
    }

And they will get a compile error on the first line if they forgot to add a path static field.

This is no different than non-static inheritance, you could make the same argument there: what's the point of an abstract method, when you will just get an error if you try and use said method when it is called directly on the concrete class.

Because instance methods aren't static. They are called polymorphically. Here:

abstract class Base {
  void doThing();
}

class Derived extends Base {
  void doThing() => print("Derived");
}

main() {
  Base base = Derived();
  base.doThing();
}

Here, there are no static calls to doThing() on Derived. The only call is a polymorphic call through the base type. Without an error on unimplemented abstract methods, there would be no compile-time checking that polymorphic calls are properly implemented.

The core value is that the code is self documenting, and we're leaning on the compiler to lower devs cognitive load. They don't need to remember all the methods AppLink's are supposed to implement they can just look at / extend the base class, and the compiler will offer them code-completion for all required methods.

But... when defining a new AppLink subclass, you also have to add some explicit calls to those static methods. Otherwise they will never be used. And the point that you add those calls, the compiler will certainly remind you if you forgot to define the static methods.

esDotDev commented 3 years ago

Well sections of code get commented out / in all the time. When creating an AppLink the first time, the developer is in the best mental space to fully define it's API. Adding them piece meal, as they are needed, but after developer has context switched, is much less efficient. A interface that can support both instance/static methods helps accomplish that goal.

Otherwise they will never be used.

No, it just means they are not used at this moment in time. They may need to be wired up by someone else, or just temporarily commented out. Consider another static property like 'path` or 'title', all Pages should have a path and title, it doesn't necessarily mean it will be used for every single page, but rather that one should always be declared as a matter of convention (in case, in the future, we need to reference it).

You may have a view that can take args, but those args aren't being passed yet for whatever reason. Maybe they are coming from another view, by another developer. So no one in the app is calling setArgs constructor on this new view. Later the links in the app are changed to pass those args, we go to wire it up, and unfortunately the developer forgot to wireup setArgs for the view because they were relying on their memory to remember add this method.

the compiler will certainly remind you if you forgot to define the static methods.

One issue with this argument, other than the fact that not every method call is always wired up, is that the function call doesn't necessarily define the contract fully. With a base method signature we can more explicitly define optional parameters, async return type etc. Otherwise every pageLink will end up with slightly different implementations of the static methods depending on which optional params were or were not passed at the moment in time the Link was created. This could quickly turn messy, and cause a lot of back-tracking as developers on the team think they have satisfied their contracts fully, when they have not. We also get no code completion help here because the methods do not actually exist yet, so opens the door nicely to spelling mistakes and other inconsistency. And forget about renaming mathod or param names for each method implementation with a simple F2 :(

Another benefit is this would help significantly with tracking down usages, I can go to the base class of AppLink, find all static usages of fromArgs, and easily update all static methods extending it, or just refactor to change the name, etc.

It seems to me that most of the benefits of declaring an interface on instance methods, also applies to static methods, so the usefulness is sort of self-evident. I mean why do we define contracts in the first place? Yes it helps with clean architecture a bit, or mocking/testing, but primarily I use it do communicate to other developers on the team (or my future self), what methods/behaviors are required when adding a specific type of object to the system, or to more easily refactor the code base, and both static and instance methods are useful to be able to define here in different contexts.

munificent commented 3 years ago

I think I understand what you're getting at.

You think of abstract members as a human-facing "contract" that you want users to have to remember to implement when subclassing. From that perspective, static members are still part of a class's "contract", so abstract ones are natural.

But I don't know if that mental model is widely shared. Abstract instance members solve concrete technical problems with using the language, not just maintainability problems. Abstract members let you specify "this member can be called on all instances of the superclass, but the superclass does not implement it." Without declaring it on the superclass, there would be no way for the type system to understand that calls are allowed. Without requiring subclasses to implement the member, there would be no guarantee that a method call will actually be able to find a concrete method to polymorphically dispatch to at runtime (like the "unimplemented pure virtual method" errors you can get in C++).

None of that applies to static members. There is no way to actually abstract over static members. There is no connection at all between between the static members of a base class and its subclasses. You may have a static method foo in a superclass and static method foo in the subclass with entirely different signatures that do entirely different things. It's entirely common to have static methods in subclasses that don't relate to the superclass at all and vice versa.

If your application wants to model some kind of abstraction over a family of classes like this, you may want to consider using something like the abstract factory pattern. Dart classes aren't really that. If we ever did metaclasses, they would be. But, if we did metaclasses, we'd also probably have abstract static methods at that point (because they would be abstract instance members on the metaclass.

petro-i commented 3 years ago

this is definitely required, de/serialization of generics in Dart/Flutter is pain now! #30074 in SDK is closed, this one no progress.

How to get out of functions, factories, map of factories and other crap in case you have de/serialize class with generics fields?

Levi-Lesches commented 3 years ago

From @danielleiszen https://github.com/dart-lang/language/issues/356#issuecomment-724360345:

abstract class Converter<T> {
  T convert(String source);
}

class Convertable<T> {
  static Converter<T> converter();
}

class Loader<T extends Convertable<T>> {
  T loadFromString(String source) {
    var converter = T.converter();

    return converter.convert(source);
  }
}

class Data with Convertable<Data> {
  @override
  static Converter<Data> converter() {
    return DataConverter(); // specific converter instance
  }
}

I find that most of the time, these static interfaces can be implemented either on instances or as callbacks. It's a different mindset entirely. For example, this is how I would write the above:

typedef Converter<T> = T Function(String source);

class Loader<T> {
  Converter<T> converter;
  Loader(this.converter);

  T loadFromString(String source) => converter(source);
  /// More `loadFromX` functions that use `converter`, like `loadFromTheCloud`
}

class Data {
  // special logic for converting strings to data
  static Data fromString(String source) => Data(source);

  final String value;
  Data(this.value);
}

void main() {
  Loader<Data> loader = Loader(Data.fromString);
  String userInput = "Hello World";
  Data dataFromInput = loader.loadFromString(userInput);
  print(dataFromInput.value);  // "Hello World"
}

Saying that "every class must have a static function called convert" is just a means to the end -- that there should be some way to convert to every class. Using callbacks does that, often with less boilerplate. I find that once you really look at static interfaces as completely separate from OOP and inheritance, you start to look at static interfaces as non-ideal, and other solutions become more attractive. I rarely use static methods now.

I do, however, agree with @esDotDev, in that I would like to be able to use static interfaces as am API specification so that I could have guidance. For example, writing a Serializable class would remind me not to forget to include a .fromJson constructor. Like @munificent said, these errors get caught sooner or later, but I still think it's helpful to have that done while writing my dataclass. Especially since responsibilities on my team are split, the person writing the dataclass, and thus in charge of the .fromJson constructor is usually not the person who ends up using the constructor in the end. Having a lint when you forget can save some time later down the line.

(Also, static metaprogramming will address a good amount of these issues -- we can solve the Serializable problem by simply writing @Serializable!).

danielleiszen commented 3 years ago

thanks for the detailed answer @Levi-Lesches I get your point

jodinathan commented 2 years ago

Our need for this is within a builder that we are designing.
We check if the model type has a specific static method by name, if yes, then we use it in the source generation.
However, it would be nice to enforce the return and argument types with a static interface.

Wdestroier commented 2 years ago

@eernstg maybe this changes your perspective on virtual static methods in Dart, maybe there are better ways to do it... (ex: extension InstructionOpcode on Type<T extends Amd64Instruction> { int get opcode { switch(T) {...} } })

abstract class Amd64Instruction {
  static List<int> get opcodes;
}

class MovInstruction extends Amd64Instruction {
  static List<int> get opcodes => [ 0x90 ];
}
eernstg commented 2 years ago

Perhaps. ;-) Have you considered this approach?:

extension Amd64InstructionVirtualStatics on Type {
  List<int> get opcodes {
    switch (this) {
      case MovInstruction: return MovInstruction.opcodes;
      // ... other subtypes of Amd64Instruction.
      default: throw "`opcodes` not implemented for the type $this";
    }
  }
}

abstract class Amd64Instruction {}

class MovInstruction extends Amd64Instruction {
  static List<int> get opcodes => [ 0x90 ];
}

void printOpcodes<T extends Amd64Instruction>() {
  print((T).opcodes);
}

void main() {
  printOpcodes<MovInstruction>();
}

It would have been nice to have a type argument on Type, and having the guarantee that the dynamic type of the Type object for any given type T will be Type<T>. In that case we could have used the following:

extension Amd64InstructionVirtualStatics<X extends Amd64Instruction>
    on Type<X> {
  // ...
}

This would ensure that we statically enforce that T is a subtype of Amd64Instruction when we call (T).opcodes. Currently we are allowed to call opcodes on any type, e.g., (int).opcodes, and that will just throw at run time.

Other than that, I think it is getting close to the original request.

[Edit: corrected T.opcodes to (T).opcodes — it is an error to call an instance member of Type using a type literal as a receiver, even when it is an extension instance method, but (T) is an expression of type Type which is not a type literal.]

esDotDev commented 2 years ago

Tacking on a extension on every type that when invoked will throw an error, is probably smelly enough code that it's not a pragmatic solution (ie, you wouldn't use this in production code, just too risky for what it gives back).

I've long wondered why Dart did not have generic constraints like you see in C#, really limits the flexibility of generics.

In addition to basic type constraints, things like new are extremely helpful https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/new-constraint

Wdestroier commented 2 years ago

Thanks, it's a possible approach. I agree that the Type class could be improved (and hopefully the class name won't be available during runtime, to help protect programs against reverse engineering). Virtual static methods would still be an interesting feature, but I'm not sure if it's a good design choice to use them (the "is static evil?" question).

eernstg commented 2 years ago

@esDotDev wrote:

Tacking on a extension on every type that when invoked will throw an error .. you wouldn't use this in production code

If Type were generic then we'd only enable the extension for subtypes of the specified bound, not for every type. So that's a huge improvement.

But there is still a missing part: The implementation of the extension member (here: opcodes) needs to perform an emulation of the OO dispatch. If the dispatching code is wrong then we will not get a compile-time error, we will get a run-time error:

extension Amd64InstructionVirtualStatics on Type {
  List<int> get opcodes {
    switch (this) {
      case MovInstruction: return MovInstruction.opcodes;
      // Oops, we forgot `AddAlImm8Instruction`.
      default: throw "`opcodes` not implemented for the type $this";
    }
  }
}

abstract class Amd64Instruction {}

class MovInstruction extends Amd64Instruction {
  static List<int> get opcodes => [ 0x90 ];
}

class AddAlImm8Instruction extends Amd64Instruction {
  static List<int> get opcodes => [ 0x04 ];
}

void printOpcodes<T extends Amd64Instruction>() {
  print((T).opcodes);
}

void main() {
  printOpcodes<AddAlImm8Instruction>(); // Throws!
}

Nevertheless, I think there's no end to the number of situations where real-world application code needs to get things correct in ways that the type system cannot follow. Maintaining a correct and complete list of switch cases covering all subtypes of a given class like Amd64Instruction is probably not the most complex situation we'll encounter. So it might actually be usable in production code after all.

lrhn commented 2 years ago

Instead of passing in types, as type arguments, that are then only used for comparison or method lookup, consider treating those "types" as enums instead.

class Amd64Instruction {
  static const Amd64Instruction mov = Amd64Instruction([0x90]);
  static const Amd64Instruction addAlImm8 = Amd64Instruction([0x04]);
  // ...

  final List<int> opcodes;
  Amd64Instruction(this.opcodes);
}

void printOpcodes(Amd64Instruction instruction) {
  print(instruction.opcodes);
}

(In short, don't use type parameters for anything other than as type arguments or in subtype checks. If you're using them for something more, you should use a real object instead of the Type object. Don't use Type object for anything at all, unless you are using dart:mirrors.)

Wdestroier commented 2 years ago

Not using Type extensions would make the code very hacky, because the class constructor verifications would need to be avoided. And not all instructions are easy to craft... Meanwhile, the instruction size is almost always the same (1 word in this case), that makes abstract static methods and extensions on types the best fit in my opinion.

Opcode

Wdestroier commented 2 years ago

I think that I got your point. I don't need to assemble the class and get the instruction size, because in reality I'll probably have the class instance.

The problem with treating all instructions as enums is that it's not possible to pass arguments to them. What I understood is that I could do this:

class _Push {
  _Push(int value) {
    if (value > 0xFFFF) throw 'an exception';
  }
}

// Somehow figure out how to push other values
const push = _Push(0);
// Would work well with a no-operation instruction...
const nop = _Nop();

I could do what you said, but I see an advantage of using extensions to separate the instructions model/data classes (a file with ~1k lines) from code that is related to the assembler (1.6k lines).

Example data class Extension

I could do what you said by separating the instructions in many files, instead of grouping code of the same functionality together...

Wdestroier commented 2 years ago

@tatumizer k I'll send you a message when I finish an important part of it, it's a fun project only. For this project, the feature discussed in this issue isn't really needed. Overall, extension methods can solve the problem very well. The only problem that I can imagine is performance. Switch statements on types will be compiled to if-else statements. Considering 100+ case statements, each case requiring 1 compare and 1 jump instruction, the performance will probably degrade a little.

JeroenvdV commented 2 years ago

The main benefit I'm searching for in my own case (and I think many others in theirs) is to be able to elegantly specify that there are some static constraints on a T, and use those properties in a generic, without having to keep some sort of second mapping of all those subclasses to their respective versions of the method. The best way to achieve that in this thread is with static extends and nobody has seemed to protest that. So then, what is stopping the addition of this feature to the language?

@eernstg put together a hugely educational (for me) piece on why static inheritance would be absurd for all inheritance, but @munificent rightfully pointed out that he hadn't considered that the static part of the inheritance could (and should) be an explicit option. Subsequently, both seemed to agree that static extends would solve the original problem.

All the code examples in this thread so far, as well as my own code, have needed some sort of list, switch statement, or other mapping that mentions each of the subclasses a second time. Others have already pointed out more specific reasons that this is undesirable, I just believe we should be able to provide information once and concisely.

This code snippet summarizes what I personally would like to be able to do, and if this is made possible I think the other presented cases also become possible.

abstract class Serializable {
  static const String tableName;
  factory fromJson(String);
}

class Product static extends Serializable {
  static const tableName = 'products';
  int weight;

  Product(this.weight);

  Product fromJson(String json) {
    int weight = ''; // TODO decode json to get data
    return Product(weight);
  }
}

class Order static extends Serializable {
  static const tableName = 'orders';
  String address;

  Order(this.address);

  Order fromJson(String json) {
    String address = ''; // TODO decode json to get data
    return Order(address);
  }
}

class SomeDatabase<T static extends Serializable> {
  SomeDatabase() {
    this._connection = connect(T.tableName); // This currently can't be done
  }

  SomeDatabaseConnection _connection;

  SomeDatabaseConnection connect(String tableName) {
    // ...
  }

  T getFirstItem() {
    return T.fromJson(_connection.getFirstItem()); // This currently can't be done
  }
}

main() {
  productDatabase = SomeDatabase<Product>()
  orderDatabase = SomeDatabase<Order>()

  print(productDatabase
      .getFirstItem()
      .weight);

  print(orderDatabase
      .getFirstItem()
      .address);
}

To reiterate, SomeDatabase should be able to know that it can call T.fromJson because I specified that T static extends Serializable and because the interface of Serializable specifies fromJson. I'm open to other similarly concise ways to write this, but in any case we shouldn't need to provide any additional mapping for Dart to find this factory.

munificent commented 2 years ago

So then, what is stopping the addition of this feature to the language?

It's really important to keep in mind that language features don't add themselves. Every language feature carries a permanent increase in language complexity with it and requires surprisingly large engineering resources to design, specify, implement (~3 times), test, document, and evangelize.

When a feature doesn't spontaneously appear in the language, that doesn't mean anyone put effort into stopping it. It means the team didn't take effort away from all of the other work it's doing and put that effort into advancing it.

In this case, I think static inferfaces is a cool, useful feature. But it's a fairly large amount of complexity (it touches the static type system which is always costly) and the demand for it is nowhere near as high as the demand for other features. I can certainly see us doing it at some point, but I think we have bigger fires to put out first.

eernstg commented 2 years ago

Just to keep in mind what the trade-off would be if we had the T is Type<T> feature:

// ----------------------------------------------------------------------
// Glue code.

class SomeDatabaseConnection {
  String getFirstItem() => '';
}

SomeDatabaseConnection connect() => SomeDatabaseConnection();

// ----------------------------------------------------------------------

abstract class Serializable {}

extension SerializableExtension<X extends Serializable> on Type<X> {
  static const _factories = {
    Product: Product.fromJson,
    Order: Order.fromJson,
  };

  X fromJson(String json) {
    var factory = _factories[this];
    if (factory == null) {
      throw new ArgumentError('Cannot do `fromJson` on $this');
    }
    return factory(json) as X;
  }
}

class Product extends Serializable {
  int weight;
  Product(this.weight);
  static Product fromJson(String json) {
    int weight = 42; // TODO decode json to get data
    return Product(weight);
  }
}

class Order extends Serializable {
  String address;
  Order(this.address);
  static Order fromJson(String json) {
    String address = 'An Address'; // TODO decode json to get data
    return Order(address);
  }
}

class SomeDatabase<X extends Serializable> {
  SomeDatabaseConnection _connection;
  SomeDatabase(): _connection = connect() {/* Init stuff */}
  X getFirstItem() => X.fromJson(_connection.getFirstItem());
}

void main() {
  var productDatabase = SomeDatabase<Product>();
  var orderDatabase = SomeDatabase<Order>();
  print(productDatabase.getFirstItem().weight);
  print(orderDatabase.getFirstItem().address);
}

It is true that we need to write our own dispatcher (that is, a mechanism which will choose a function from a set of possible functions based on the type of the receiver), and that does give rise to an extra burden during maintenance. However, client code does get to look as requested, and we do get good type safety.

We should introduce a subtype of Serializable, let's just call it MySerializable, which is a common supertype of all the types that we wish to support with fromJson, and not a supertype of anything else. That would ensure that we have support (in _factories) for exactly the right set of classes.

There is a downcast in the extension, factory(json) as X, but that downcast is guaranteed to succeed as long as we ensure that _factories maps a type T to a function with signature T Function(String). As long as we maintain this invariant in _factories, and if we use the bound MySerializable in the extension rather than Serializable (such that we can't call fromJson on any other types than the subtypes of MySerializable), it is all type safe.

JeroenvdV commented 2 years ago

@eernstg Sorry for editing my post before checking for replies, what I just added to my code example is a second static member denoting the tableName for the Product and Order. Isn't it true in your example that in addition to _factories, we would now need a second map called _tableNames which basically almost duplicates it? In the case where there is more than one static member to implement, the emulation using Companions you first designed might be better in that regard. I personally wouldn't mind having to write Serializable.companion<T>() rather than T. It's just that when I write a new class that static extends MySerializable, I wouldn't want to have to also add a line to n different dispatchers where n is the amount of static interface members of MySerializable. With a companion, that is reduced to adding 1 companion per subclass and adding 1 companion to the list of companions, which I think is still worth improving to none with static extends.

Regarding the T is Type<T> feature; are any of its use cases also solved by static extends? I also wonder how they compare on the cost to implement / utility and demand plane. Speaking of which, @munificent I didn't mean to make light of the effort needed to make changes, or other changes that are more important. Sorry for that. I was just wondering if this thread is still about finding a workable technical solution to the problem, or whether we are at a point where there is a clear proposal that can be considered and fine-tuned. If I'm not mistaken, it seems to be leaning towards the latter.

eernstg commented 2 years ago

when I write a new class that static extends MySerializable, I wouldn't want to have to also add a line to n different dispatchers

It is indeed true that we need a dispatcher for each method, and also that the companion approach uses normal OO dispatch, so we just need the single "object dispatcher" that will deliver the companion object itself. On the other hand, we need to look up the companion (using a user-written dispatcher) and then look up the implementation of the target method (using built-in OO dispatch), so it's a bit slower.

The approach using an extension on Type<X> can of course use a hierarchy of companion objects and perform the lookup of the companion object, so it wouldn't be difficult to make an approach that uses companion objects and an approach that uses separate user-written dispatchers look the same from the client point of view.

One thing is different, however: The approach based on Type<X> makes the type a first class entity. For example, we will be able to store the type, e.g., in a list of types, and use the type to invoke one of these "pseudo-static methods". With the companion approach the type is not first class, it needs to be available as a type literal (so it's known at compile time) or a type variable (so it must occur in the scope of that type variable, because we can't extract the value of a type variable from an existing object).

So we'd be in trouble if we want to use the companion object approach, and we wish to compute a set of types, and then we wish to create an instance of each of the elements of that set (using fromInteger, or fromJson, or whatever). There is no way to provide access to a set of types as type variables: Every piece of code in a Dart program is in a scope; that scope has a fixed number of type variables that are in scope, say k type variables (in lots of locations k is zero), and then we cannot represent a set of size k + 1. So we'd need to be innovative to do that at all. (We could create a generic function whose body does what we want, and then create a generic function instantiation where we specify the value of the type argument of that function, and then we'd pass around a set of functions representing the set of types; but we would then only be able to perform that single operation which is the pre-chosen code in the body of the function).

Regarding the T is Type<T> feature; are any of its use cases also solved by static extends?

I think we would need to dive deep into all the potential consequences of a static extends proposal in order to understand what it would mean, which features it would have, and which implications it would have for performance and expressive power. Adding a completely new (and orthogonal) kind of bound on type variables could be as heavy as null safety.

So I tend to think that the question isn't whether we could "simply" do static extends, and then we wouldn't need T is Type<T>. The question is rather whether we could do static extends at all, with the given resources.

Here's an updated version of the companion object example that shows how we can preserve the static typing with that model at a very similar level as that of the T is Type<T> approach, and also showing that we can (of course) use companion objects behind the scenes with the T is Type<T> approach, if we wish to do that:

// ----------------------------------------------------------------------
// We can use the `T is Type<T>` approach here, too.

extension SerializableExtension<X> on Type<X> {
  X fromInteger(int i) =>
      Serializable._companion[this]!.fromInteger(i) as X;
}

// ----------------------------------------------------------------------

abstract class Serializable {
  static const Map<Type, SerializableCompanion> _companion = {
    Serializable: const SerializableCompanion(),
    A: const ACompanion(),
  };

  static SerializableCompanion<X> companion<X extends Serializable>() =>
      _companion[X]! as SerializableCompanion<X>;
}

class SerializableCompanion<X extends Serializable> {
  const SerializableCompanion();
  X fromInteger(int i) => throw "AbstractInstantionError";
}

class A implements Serializable {
  int foo;
  A(this.foo);
}

class ACompanion implements SerializableCompanion<A> {
  const ACompanion();
  A fromInteger(int i) => A(i);
}

T bar<T extends Serializable>(int nb) {
  return Serializable.companion<T>().fromInteger(nb);
}

main() {
  var wow = bar<A>(42);
  print(wow.foo);
}
Wdestroier commented 2 years ago

I'd vote to avoid turning types into first-class entities...

eernstg commented 2 years ago

I'd vote to avoid turning types into first-class entities...

There is no proposal here to change the language in this area, I just mentioned that we're using this feature. The only enhancement in the Type<T> proposal is that it improves the static type safety and avoids ambiguity conflicts among extensions.

kevmoo commented 1 year ago

C# is shipping this in v11

See https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/static-virtual-interface-members A great overview here https://www.youtube.com/watch?v=v8bqAm4aUFM

insinfo commented 9 months ago

I would love to see this feature in dart, this will help in various use cases like serialization, it would be similar to what is available in C#

which would allow the jsonEncode and jsonDecode functions to work for any custom object that implements the MapSerializable interface

Maybe due to dart current limitations, I think this could only be applied to data classes in the future

example

abstract interface class MapSerializable<T> {
  Map<String, dynamic> toMap();
  static T fromMap(Map<String, dynamic> map);
}

data class Person implements MapSerializable<Person>{
  String name;
  Person(this.name);

  @override
  Map<String, dynamic> toMap() {
    return {'name':name};
  }

  @override
  static Person fromMap(Map<String, dynamic> map){
    return Person(map['name']);
  }
}

void main(List<String> args) {
  var p = Person('John Doe');
  print(jsonEncode(p));
  //{"name":"John Doe"}
  Person person = jsonDecode<Person>('{"name":"John Doe"}');
  print(person.name);
  //'John Doe'
}

https://www.php.net/manual/en/class.jsonserializable.php

desmond206x commented 4 months ago

Any progress here?