language icon indicating copy to clipboard operation
language copied to clipboard

Abstract static methods

Open HugoKempfer opened this issue 5 years ago • 59 comments

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);
}

HugoKempfer avatar May 16 '19 14:05 HugoKempfer

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.

MarvinHannott avatar May 17 '19 09:05 MarvinHannott

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.

HugoKempfer avatar May 17 '19 12:05 HugoKempfer

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.

MarvinHannott avatar May 17 '19 13:05 MarvinHannott

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 avatar May 21 '19 16:05 eernstg

@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'.
    }
}

eernstg avatar May 21 '19 16:05 eernstg

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.

munificent avatar May 21 '19 22:05 munificent

@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.

eernstg avatar May 24 '19 08:05 eernstg

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.

munificent avatar May 28 '19 22:05 munificent

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.

eernstg avatar May 29 '19 18:05 eernstg

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.

munificent avatar May 29 '19 20:05 munificent

It'll really useful feature for serializable classes.

rodion-m avatar Sep 19 '19 13:09 rodion-m

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.

AirborneEagle avatar Feb 05 '20 19:02 AirborneEagle

Now it's 2020,Is there any progress?

asjqkkkk avatar Feb 20 '20 01:02 asjqkkkk

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

Manuelbaun avatar Mar 23 '20 19:03 Manuelbaun

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);
}

Peng-Qian avatar Jun 24 '20 01:06 Peng-Qian

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.

munificent avatar Jul 10 '20 23:07 munificent

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.

danielleiszen avatar Nov 10 '20 00:11 danielleiszen

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.

gisborne avatar Jan 06 '21 06:01 gisborne

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.

esDotDev avatar Mar 07 '21 00:03 esDotDev

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.

munificent avatar Apr 21 '21 00:04 munificent

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.

esDotDev avatar Apr 21 '21 05:04 esDotDev

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);

rrousselGit avatar Apr 21 '21 09:04 rrousselGit

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<T extends InheritedWidget> on T { static T of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<T>(); } }

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.

gisborne avatar Apr 21 '21 20:04 gisborne

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.

danielleiszen avatar Apr 22 '21 02:04 danielleiszen

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.

munificent avatar Apr 22 '21 23:04 munificent

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.

esDotDev avatar Apr 23 '21 00:04 esDotDev

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.

munificent avatar Apr 24 '21 01:04 munificent

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?

petro-i avatar Jul 07 '21 07:07 petro-i

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!).

Levi-Lesches avatar Jul 07 '21 08:07 Levi-Lesches

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

danielleiszen avatar Jul 07 '21 09:07 danielleiszen