Language: Support for abstract static methods

Created on 16 May 2019  路  17Comments  路  Source: dart-lang/language

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

request

Most helpful comment

It'll really useful feature for serializable classes.

All 17 comments

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.

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.

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.

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.

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

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

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.

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.

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.

It'll really useful feature for serializable classes.

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.

Now it's 2020,Is there any progress?

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

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

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.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dev-aentgs picture dev-aentgs  路  3Comments

panthe picture panthe  路  4Comments

eernstg picture eernstg  路  5Comments

creativecreatorormaybenot picture creativecreatorormaybenot  路  3Comments

wytesk133 picture wytesk133  路  4Comments