Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract static methods #356

Open
HugoKempfer opened this issue May 16, 2019 · 59 comments
Open

Abstract static methods #356

HugoKempfer opened this issue May 16, 2019 · 59 comments
Labels
request Requests to resolve a particular developer problem

Comments

@HugoKempfer
Copy link

HugoKempfer commented May 16, 2019

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
Copy link

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
Copy link
Author

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
Copy link

MarvinHannott commented May 17, 2019

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
Copy link
Member

eernstg commented May 21, 2019

Cf. an old SDK issue on a similar topic: dart-lang/sdk#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
Copy link
Member

eernstg commented May 21, 2019

@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
Copy link
Member

munificent commented May 21, 2019

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
Copy link
Member

eernstg commented May 24, 2019

@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
Copy link
Member

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
Copy link
Member

eernstg commented May 29, 2019

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
Copy link
Member

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
Copy link

It'll really useful feature for serializable classes.

@AirborneEagle
Copy link

AirborneEagle commented Feb 5, 2020

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
Copy link

Now it's 2020,Is there any progress?

@Manuelbaun
Copy link

Manuelbaun commented Mar 23, 2020

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
Copy link

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
Copy link
Member

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.

@kevmoo kevmoo added the request Requests to resolve a particular developer problem label Jul 30, 2020
@danielleiszen
Copy link

danielleiszen commented Nov 10, 2020

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
Copy link

gisborne commented Jan 6, 2021

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
Copy link

esDotDev commented Mar 7, 2021

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
Copy link
Member

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
Copy link

esDotDev commented Apr 21, 2021

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
Copy link

rrousselGit commented Apr 21, 2021

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

@Wdestroier
Copy link

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

@eernstg
Copy link
Member

eernstg commented May 1, 2022

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
Copy link
Member

kevmoo commented Aug 25, 2022

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
Copy link

insinfo commented Oct 9, 2023

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
Copy link

Any progress here?

@eernstg
Copy link
Member

eernstg commented Feb 16, 2024

Looks like I never spelled out how it is possible to use standard object oriented techniques to achieve something that is quite similar to virtual static methods by creating a shadow hierarchy of singletons, playing the role as "the static member holders" of the primary hierarchy.

This is in particular something you can do if you "own" the entire hierarchy (that is, if you have marked all of the classes as final, or something equally strong), because it relies on creating a global mapping (_companion below).

abstract final class Serializable {
  static const Map<Type, _SerializableCompanion> _companion = {
    Serializable: const _SerializableCompanion<Serializable>(),
    A: const _ACompanion<A>(),
    B: const _BCompanion<B>(),
  };

  static _SerializableCompanion<X> type<X extends Serializable>() =>
      _companion[X] as _SerializableCompanion<X>;
}

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

final class B implements Serializable {
  void memberOfB() => print('Running memberOfB!');
}

class _SerializableCompanion<X extends Serializable> {
  const _SerializableCompanion();
  X fromInteger(int i) => throw "AbstractInstantionError";
  X fromNothing() => throw "AbstractInstantionError";
  void virtualStaticMethod() => print("Most general virtual static method.");
}

class _ACompanion<X extends A> extends _SerializableCompanion<X> {
  const _ACompanion();
  X fromInteger(int i) => A(i) as X;
  X fromNothing() => A(0) as X;
  void virtualStaticMethod() => print("Virtual static method for `A`.");
}

class _BCompanion<X extends B> extends _SerializableCompanion<X> {
  const _BCompanion();
  X fromNothing() => B() as X;
}

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

main() {
  // Similar to dependency injection.
  var TypeA = Serializable.type<A>();
  var TypeB = Serializable.type<B>();

  // Companion methods resemble static methods or constructors.
  var a = TypeA.fromNothing();
  var b = TypeB.fromNothing();
  a = TypeA.fromInteger(42); // We have more than one way to create an `A`.

  try {
    // We may or may not have support for some constructors with some types.
    Serializable.type<B>().fromInteger(-1);
  } catch (_) {
    print("We can't create a `B` from an integer.\n");
  }

  // `a` is an `A`, and it gets the type `A` by inference.
  print('a is A: ${a is A}.');
  print('a.memberOfA: ${a.memberOfA}.');

  // `b` is similar.
  print('b is B: ${b is B}.');
  b.memberOfB();
  print('');

  // Call a static method on the given type `X`.
  void doCall<X extends Serializable>() {
    // Note that we do not know the value of `X` here.
    Serializable.type<X>().virtualStaticMethod();
  }

  doCall<A>(); // Calls "A.virtualStaticMethod".
  doCall<B>(); // Calls "Serializable.virtualStaticMethod".
}

If you do not own the entire hierarchy then it's going to be less straightforward to create _companion (you'd need to ask everybody to register their subtypes of Serializable in this map, and the companion classes would have to be public).

This is probably not 'progress', but it may still be useful to be aware of. ;-)

@Levi-Lesches
Copy link

@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. Already the signature conflicts could be difficult to handle.

What about a keyword, similar to @munificent's proposal? How about adding abstract static to an interface to force that method to be overridden by every subclass? This could avoid the problem of needing to declare and extend from two separate interfaces, and tie them together more logically. Here's an example:

/// Ensures subclasses have both a [fromJson] constructor and [toJson] method.
abstract class JsonSerializable {
  /// Normal constructors and static members work as they do today. 
  static void alwaysThrows() => throw UnimplementedError(); 

  /// This constructor must be overridden in all subclasses because `abstract` was used.
  abstract static JsonSerializable.fromJson(Map json);

  /// A normal abstract method
  Map toJson();
}

class User extends JsonSerializable {
  final String name;
  
  @override  // <-- must be overridden
  User.fromJson(Map json) : name = json["name"];

  @override
  Map toJson() => {"name": name};

  @override
  bool operator ==(Object other) => other is User && other.name == name;
}

bool testJson<T extends JsonSerializable>(T obj) => T.fromJson(obj.toJson()) == obj;

@lrhn
Copy link
Member

lrhn commented May 8, 2024

What about a keyword, similar to @munificent's proposal? How about adding abstract static to an interface to force that method to be overridden by every subclass?

First of all, for the idea to work, the type JsonSerializable itself must implement the fromJson constructor, not just declare it abstractly. It's possible (based on the type constraints) to call

   var user = User.fromJson({"name": "foo"});
   testJson<JsonSerializable>(user); // Calls `JsonSerializable.fromJson`.

The JsonSerializable type can obviously implement fromJson so by throwing an UnsupportedError, but so can any other subclass, and then it's not so different from just not implementing it at all.
Which means that even if we can enforce that all subclasses has a method or constructor, we can't actually force them to implement it meaningfully. And there will be cases where they don't want to, or cannot.

Can a class opt out, fx an abstract class or interface that doesn't intend to have a generative constructor at all?
Since you cannot call the constructor directly on an abstract class, does it matter that it doesn't implement it?
But what about static methods then, which you can call?

The way JsonSerializable itself doesn't have an implementation suggests that there will be such classes.
And then we're back to "implementing" it by throwing, if we enforce that there must be an implementation when it's not desired.

This also effectively introduces a kind of "Self type" in that it can generalize over constructors which return something of their own type.
It also means that, fx, a copy constructor isn't really possible.

class Copyable {
  abstract static Copyable.copy(Copyable self);
}
class Duplicati implements Copyable {
  final int state;
  Duplicati(this.state);
  @override
  Duplicate copy(Duplicati other) : this(other.state);
}

This override has the wrong signature, it should accept any Copyable, but it only accepts Duplicati.
That means that we need either to design it as:

class Copyable<T extends Copyable<T>> {
  abstract static Copyable.copy(T self);
}
class Duplicati implements Copyable<Duplicati> {
  final int state;
  Duplicati(this.state);
  @override
  Duplicate copy(Duplicati other) : this(other.state);
}

T clone<T extends Copyable<T>>(T original) => T.copy(original);

or we may need to allow convariant overrides:

  Duplicate copy(covariant Duplicati other) => Duplicate(other.state);

which is a weird concept, and pretty unsafe, so probably not what we want.

So either F-bounded polymorphism (with its issues) or unsafe code, if we allow abstracting over functions that take themselves as argument. Or a lack of ability to have a static method which returns the same type.
(I guess both factory and generative constructors can satisfy the requirement, since this is all about calling the constructor. We can't extend a type variable, so no

class C<T extends JsonSerializable> extends T {
  C(Object? v) : super.fromJson(v); /// Calls the known-to-exist `T.fromJson`
}

But what about mixins:

mixin M on JsonSerializable {
  final int bananas;
  M(this.bananas, Object? o) : super.fromJson(o);
  String toJsonString() => toJson().toString();
}

If we know that the super-class has a specific constructor, can we then allow a mixin to have a constructor?
Then the mixin application would get the mixin's declared constructors instead of just forwarding all the superclass constructors. It could work.
If we don't know anything about superclass constructors, then the mixin cannot declare a constructor either
(but also wouldn't have to, if there are no inherited constructor requirements - and if it declares no constructors, it can still inherit/forward to every superclass constructor, and satisfy an inherited constructor requirement.).

Still, requiring every subclass to implement the same static interface is bothersome.
Unless we make them inherited, like instance methods. That's what we're mimicing with the @override, so why not take the full step.

Virtual statics

A virtual static method is a static method declared on a type, which is inherited by all subtypes. A subclass may override the member, but must do so with a signature that is a valid override of the inherited member, just like an instance member. Invoking a virtual static on a type is just like invoking a static. Invoking a virtual static on a type variable will use the static declared or inherited by the actual type bound to the type variable at runtime.

A virtual constructor is a constructor which must be implemented by all subtypes. It is not inherited, since the constructor must return the self-type. It cannot be omitted, even on abstract types, but it'll have to be a factory constructor on those. (Mixins can have factory constructors!)
It would be nice to not have to declare the constructor on an abstract class, but an abstract class can be a type argument, so if it's possible to call the constructor on a type argument, it must be there.

If we require that a generative virtual constructor is overridden by another generative virtual constructor, not a factory, then we can allow mixins with on types to have constructors that call a superclass generative virtual constructor. If we don't require that, mixins still can't know which generative constructors their superclasses have.

It's annoying to have to implement constructors on every subclass, even those which don't need it.
So what if we made the classes opt-in to a static interface, rather than having it follow the normal subtyping rules.

Static interfaces

That is, if we're going to make methods callable on type variables, I'd rather introduce static interfaces that class can choose to implement, but which is not tied to its normal interfaces.

static interface Copyable<T extends Copyable<T>> {
  Copyable.copy(T original);
}

class Duplicati implements static Copyable<Duplicati> {
  final int state;
  Duplicate(this.state);
  @override // if you want to write it
  Duplicati.copy(Duplicati original) : this(original.state);
}

T clone<T extends static Copyable<T>>(T value) => T.clone(value);

Here the <T static implements Copyable<T>> puts a restriction on which types can be passed as T, one which is not subtype based. It's a further restriction that any concrete type argument must have (the T may also have an extends SomeClass bound), which means that someone writing clone<Foo> must ensure that Foo satisfies the restriction. Which it does if it's a concrete class which satisfies the restriction, or if it's a type variable which is already restricted.

Then we can define sub-static-interfaces

static interface MultiCopy<T extends MultiCopy<T>> implements Copyable<T> {
  static List<T> copyAll(Iterable<T> originals);
}
class Triplicati static implements MultiCopy<Triplicati> {
  final int state;
  Triplicati(this.state);
  Triplicati.copy(Triplicati original) : this(original.state);
  static List<Triplicati> copyAll(Iterable<Triplicati> originals) => [for (var o in originals) Triplicati.copy(o)];
  String toString() => "<$state>";
}
Set<T> setCopy<T static implements Copyable<T>(Iterable<T> values) => {for (var value in values) T.copy(value)};
List<List<T>> deepCopy<T static implements MultiCopy<T>>(List<List<T>> lists) =>
  [for (var list in lists) T.copyAll(list)];

void main() {
  var trips = [Triplicati(1), Triplicati(2)];
  var tripOfTrips = [trips, trips];
  print(setCopy(trips)); // {<1>, <2>}
  print(deepCopy(tripOfTrips)); // [[<1>, <2>], [<1>, <2>]];
}

A subclass of a class that implements a static interface, doesn't have to implement the same static interface.
It can't be used as a type argument where that static interface is required.

abstract class JsonEncodable {
  Object? toJson();
}
static interface JsonDecodable {  
  JsonDecodable.fromJson(Object? _);
}

Here JsonEncodable is a normal interface, which means that any concrete subclass must implement it.
The JsonDecodable is a static interface, which means no class implements it without doing so explicitly.

Then you can write:

class JsonSerializer<T extends JsonEncodable static implements JsonDecodable> {
  T decode(Object? source) => T.fromJson(source);
  Object? encode(T value) => value.toJson();
}

(We'd probably want to give a name to that kind of combination constraint. Maybe typedef JsonCodable = JsonEncodable & static JsonDecodable;. But that's not a type, it's a composite type constraint, which is something we don't currently have a separate concept for, because currently a bound is always a single type.)

It's a kind of intersection type on types themselves, but it's only intersection static interfaces on top of a single type.

(Proposed as-is. All syntax subject to change. No guarantees of soundness. May contain nuts.)

I start to get why Kotlin defined companion objects as objects with normal methods and able to implement interfaces. Not that it solves everything, you still cannot get the companion object from a generic type parameter.

@Levi-Lesches
Copy link

Levi-Lesches commented May 8, 2024

First of all, for the idea to work, the type JsonSerializable itself must implement the fromJson constructor, not just declare it abstractly. It's possible (based on the type constraints) to call

   var user = User.fromJson({"name": "foo"});
   testJson<JsonSerializable>(user); // Calls `JsonSerializable.fromJson`.

The JsonSerializable type can obviously implement fromJson so by throwing an UnsupportedError, but so can any other subclass, and then it's not so different from just not implementing it at all. Which means that even if we can enforce that all subclasses has a method or constructor, we can't actually force them to implement it meaningfully. And there will be cases where they don't want to, or cannot.

Good points, I didn't think of those. But:

  • any instance method can also be overridden by a "non-cooperative" subclass that throws Unimplemented, returns a negative number when a positive is expected, ignores its parameters, etc. It's a freedom, not something that needs to be enforced, and in any case, can already be bypassed today.
  • Since static methods are called, well, statically, can't the analyzer catch these? Say JsonSerializable.fromJson was kept abstract and not implemented. testJson() is defined in terms of T extends JsonSerializable, so it's impossible to tell at the declaration site that this can cause problems... but eventually, testJson<JsonSerializable>() is invoked, whether T is explicitly used or inferred.

So at the call site, the compiler can tell that an abstract method will be used and flag it. This is a case that doesn't need to be handled today -- it's impossible to get an instance of an abstract class -- but there is a similar example, calling super.method() on a subclass of an abstract class. The compiler can see that there is no case where the parent class implements that method. So too, at the call site, the compiler should be able to tell that testJson will use a static method that just isn't declared. But if that's too complicated, the throw Unimplemented() solution works too


It also means that, fx, a copy constructor isn't really possible.

Putting the generic on the class works, but is annoying, as you said. What about putting it on the method itself?

class Copyable {
  abstract static T copy<T extends Copyable>(T self);
}

Note this isn't a constructor anymore but it's still called and used the same way.


Still, requiring every subclass to implement the same static interface is bothersome.
Unless we make them inherited, like instance methods. That's what we're mimicing with the @override, so why not take the full step.

Agreed, the usual semantics with extends and implements could apply here as well.


That is, if we're going to make methods callable on type variables, I'd rather introduce static interfaces that class can choose to implement, but which is not tied to its normal interfaces.

Seems like what @munificent was suggesting earlier in the thread. My main problems with that are:

  1. Class declarations and generic functions can get pretty verbose, even with sugar like &
  2. This notion of "composite types" is a new concept entirely and will require more work (see Multiple upper bounds #2709)
  3. A static interface can't depend on a normal interface and vice-versa. In your example, you had to split JsonSerializable into JsonDecodable and JsonEncodable. You can't write an interface that works with testJson, and therefore it wouldn't work with Firestore either, which forces you to provide a toJson and fromJson for every collection. Situations like these are the primary motivation for abstract interfaces. Like you said, you could do:
abstract class JsonEncodable { Map toJson(); }
static interface JsonDecodable { JsonDecodable.fromJson(); }
typedef JsonSerializable = JsonEncodable & static JsonDecodable;

But then what about methods like:

JsonSerializable copyWith(String key, Object? value) {
  final json = toJson();
  json[key] = value;
  return fromJson(json);
}

Where would that go? JsonEncodable doesn't have .fromJson, and JsonDecodable doesn't have toJson. You'd have to make a third type like JsonSerializable... and then you're back to square one, where every subclass must have both. It would be far more natural to have subclasses inherit both the instance and static interfaces.

In general, I'm not really as sympathetic to "what if the subclasses don't want it?" because by using abstract static, the parent class/interface is demanding static methods be implemented as part of its interface. As with instance interfaces, you can have "mischievous" subclasses that do just enough to compile but break expected functionality, and there's no real way to stop them in the instance or static cases. But at least we can be clear about what the expected behavior is and abstract over both static and instance members in doing so.


As a final example, consider Cloud Firestore's CollectionReference.withConverter

typedef FromFirestore<T> = T Function(snapshot, options);
typedef ToFirestore<T> = Map<String, dynamic> Function(T value, options);

CollectionReference<R> withConverter<R extends Object?>({
  required FromFirestore<R> fromFirestore,
  required ToFirestore<R> toFirestore,
});

class User {
  User.fromFirestore(snapshot, options) { ... }
  Map toFirestore(options) => { ... };
}

final users = firestore.collection("users").withConverter(
  fromFirestore: User.fromFirestore,
  toFirestore: (value, options) => value.toFirestore(options),
);

This could instead be:

abstract class FirestoreObject {
  abstract static FirestoreObject fromFirestore(snapshot, options);
  Map<String, dynamic> toFirestore(options);
}

CollectionReference<T extends FirestoreObject> withConverter();

class User extends FirestoreObject {
  @override 
  static User fromFirestore(snapshot, options) { ... }

  @override 
  Map toFirestore(options) => { ... };
}

final users = firestore.collection("users").withConverter<User>();

@insinfo
Copy link

insinfo commented May 8, 2024

think that the syntax of the class that is inheriting should remain the same, that is, keeping the reserved word static in the inherited method

that is, focusing on overriding and/or implementing static methods, and perhaps leaving constructors aside in abstract interfaces

abstract class FirestoreObject {
  abstract static FirestoreObject fromFirestore(snapshot, options);
  abstract Map<String, dynamic> toFirestore(options);
}

CollectionReference<T extends FirestoreObject> withConverter();

class User implements FirestoreObject {
  @override 
  static User.fromFirestore(snapshot, options) { ... }
  @override 
  Map toFirestore(options) => { ... };
}

@lrhn
Copy link
Member

lrhn commented May 9, 2024

Since static methods are called, well, statically, can't the analyzer catch these?

The point of virtual abstract methods is that they are not resolved statically.
If you call T.fromJson on a type variable, then the code should run the fromJson of the type bound to T at runtime.

All the compiler can see is a type parameter with a bound <T extends JsonSerializable> and somewhere else T.fromJson.
That can cause two responses:

  • Disallow (make a compile-time error) T.fromJson because (maybe) not all subclasses of JsonSerializable actually has a fromJson (including JsonSerializable itself), or
  • Cause a runtime error at T.fromJson if the current type for T has no T.fromJson.

Since the goal here is to allow T.fromJson, we need to either:

  • Accept that the call can throw, and make it a user issue to only pass compatible types as arguments.
  • Enforce that all subclasses has a fromJson. If they then choose to throw anyway, then that brings us back to it being a user issue, but it won't be because a user forgot to write a fromJson.
  • Add further restrictions on the type parameter to disallow any type that doesn't implement fromJson.

My "static interfaces" idea above is the third item. I'd at least go for the second item. Number one is just too unsafe.

class Copyable {
 abstract static T copy<T extends Copyable>(T self);
}

Not sure how that would be implemented. It means that every Copyable class must have a method that can copy every Copyable object. If both Document and DanceStyle are Copyable, nothing in the signature prevents me from doing Document.copy<DanceStyle>(someDanceStyle), which Document surely doesn't know anything about.

I agree that static interfaces are probably too complicated, because they don't mix with normal interfaces, so you can't require a type to implement a static interface.

If we make virtual static methods be carried along with the interface, like instance methods, then:

  • All subtypes will have the virtual static methods. It's a compile-time error for a type to not satisfy the virtual static signatures of its superclasses.
  • We can inherit statics along extends relations.
  • We can even inherit constructors. If a virtual constructor isn't mentioned, an automatic forwarder will be added. (If that's not enough, because it doesn't initialize the current class, then that's a compile-time error, but simple forwarding is automatic.)

It still means that JsonSerializable must implement its own interface, including the fromJson constructor that nobody can call because the type is abstract.
Which suggests one extra constraint on type parameters: Not being abstract.

Consider:

  T copyWith<T extends new JsonSerializable>(T object, String key, Object? value) =>
     T.fromJson(object.toJson()..[key] = value);

The new in front of the type requires the type to be non-abstract. That is what allows calling a constructor on it.

Or we can just require every subclass of JsonSerializable to have a callable constructor called fromJson, some will just have to be factory constructors. Which likely throw.

Or we can introduce structural constraints instead of static interfaces:

  T copyWith<T extends JsonSerializable{T.fromJson(Object?)}>(T object, String key, Object? value) =>
     T.fromJson(object.toJson()..[key] = value);

which means copyWith can only be called with a T that has a fromJson constructor. It's not a static interface that you can name, which again means there is no way to enforce that every subclass of JsonSerializable has one, but if every place the type is going also requires the static constraint, then it'll probably be added soon enough.

(But then there is surely no way to promote to satisfy such a constraint, which would be something like if (o is JsonSerializable{new.fromJson(Object?)}) return copyWith(o);. I don't want to do runtime reflection of static methods of the runtime type of an object.)

So yes, "all subtypes must implement static virtual members too" is probably the only thing that can fly, and abstract classes will just have to implement constructors as factories.

@Levi-Lesches
Copy link

Not sure how that would be implemented. It means that every Copyable class must have a method that can copy every Copyable object. If both Document and DanceStyle are Copyable, nothing in the signature prevents me from doing Document.copy<DanceStyle>(someDanceStyle), which Document surely doesn't know anything about.

Very good point 😅. Yeah, the issue of having a self type is a bit hard to avoid. I'm still not sure I understand a path forward without the standard trick of class X<T extends X<T>>, but I hope I'm missing something.

My "static interfaces" idea above is the third item. I'd at least go for the second item. Number one is just too unsafe.

Totally agree. I don't love the structural constraints, it's way too different and the name is important imo.

Which suggests one extra constraint on type parameters: Not being abstract.

I kinda like this idea. I can see why you would want to allow an abstract class though, as many abstract classes have concrete factory constructors that pick a preferred concrete subclass to construct.

@insinfo
Copy link

insinfo commented May 11, 2024

Couldn't the dart implementation be similar to the C# Static abstract members implementation, with just a few syntax differences such as replacing the ":" with "extends" among other small differences from the dart style instead of the C# code style?

c#

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 :, IAddable<Int32>
{
    static Int32 I.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/static-abstracts-in-interfaces

@Levi-Lesches @lrhn
This link has the specification and discussion about the design
https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/static-abstracts-in-interfaces.md
dotnet/csharplang#4436

@lrhn
Copy link
Member

lrhn commented May 11, 2024

The C# design is basically that interfaces can have static members too, and implementing classes have to implement those static members.

They solve the issue of interfaces, abstract classes in Dart, not implementing the interface, by not allowing an interface type as argument to a type parameter which has a type with static members as bound.
An alternative could be to mark the type parameter specifically if it's allowed to use static members, and then only make the restriction for those type variables. (But that's probably not worth it, why use a type with static members as part of its API if you're not using it. Well unless we start adding superclasses with statics to existing types like num, which we could, but since Dart operators are instance methods, not static, it's probably not as relevant as in C#.)

C# already has a separate way to specify constructors on type parameters, so they don't need to incorporate the self type into the static interface to support constructors. On the other hand, Dart should be able to just assume that a constructor on type variable T returns a T. If abstract classes are not allowed, we know that we can invoke constructors on the type variable.

C# also has virtual static methods. It's not necessary for having abstract static methods, if needed it can be added later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests