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

Static Interfaces in dart #2089

Closed
avdosev opened this issue Feb 2, 2022 · 8 comments
Closed

Static Interfaces in dart #2089

avdosev opened this issue Feb 2, 2022 · 8 comments
Labels
feature Proposed language feature that solves one or more problems state-duplicate This issue or pull request already exists

Comments

@avdosev
Copy link

avdosev commented Feb 2, 2022

Introduction

Sometimes there are situations when it would be great to write something like this:

Value maker<T extends StaticInterface<Value>>() {
     // some operations
     final arg1 = ...;
     final arg2 = ...;
     return T.create(arg1, arg2);
}

Where static interface is:

abstract class StaticInterface<T> {
     static T create(int arg1, String arg2); 
}

Motivation

Recently I worked on a tool for loading data from a database. When you work with a database, you know the type of loaded value, but you need the same unified method for data deserialization for different types. Currently, you can use the Factory pattern:

class FromJsonFactory {
  static T fromJson<T>(Map<String, dynamic> json) {
    switch (T) {
      case Type1:
        return Type1.fromJson(json) as T;
      case Type2:
        return Type2.fromJson(json) as T;
      case Type3:
        return Type3.fromJson(json) as T;
      default:
        throw UnimplementedError(
          "FromJsonFactory is undefined for ${T.toString()}",
        );
    }
  }
}

With static interfaces this could be much shorter:

abstract class IFromJson<T> {
   static T fromJson(Map<String, dynamic> json);
}

class FromJsonFactory {
  static T fromJson<T extends IFromJson<T>>(Map<String, dynamic> json) {
    return T.fromJson(json);
  }
}

Project with same problems

Hive - they use Hive.registerAdapter but could use this feature
more about registerAdapter

@avdosev avdosev added the feature Proposed language feature that solves one or more problems label Feb 2, 2022
@Levi-Lesches
Copy link

This looks like a duplicate of #356 (granted, it's hard to find). It's also discussed in several related issues, like #2039 and #1787.

For some quick context, see this comment where I summed up the big issues of introducing static inheritance.

@munificent munificent added the state-duplicate This issue or pull request already exists label Feb 2, 2022
@avdosev
Copy link
Author

avdosev commented Feb 2, 2022

@Levi-Lesches Thank you, but is it really necessary to have abstract static methods in order to call a static method in a generic function? It is used in my example, but this functionality can be implemented without inheriting static methods.

It is enough to add the ability to check whether a type has a specific static method. This can be implemented in various ways, for example, via concepts like in C++ so it would look like this:

concept CSerializable<T> {
    static T fromJson(dynamic json);
    dynamic toJson();
}

But the concept can only be used in generics or used with 'implement' key word in class implementation:

T parser<T extends CSerializable<T>> {
    return T.fromJson(...)
}

@Levi-Lesches
Copy link

Your idea of "concepts" is exactly equivalent to abstract classes: a description of an interface that you can later check if another class implements. In fact, if you replace the word concept with abstract in your example, it almost exactly matches the code sample in #356.

If you mean we should use another word to mean static inheritance, so as not to break everything, that's discussed in this comment in #356 and, in a different sense, in #42/#723.

If you mean a way to check for a static interface, but not enforce its inheritance, that's discussed in #2039 and #1787.

@eernstg
Copy link
Member

eernstg commented Feb 3, 2022

@avdosev, over a long period of time we have discussed a particular language feature whereby the built-in type Type gets a type argument, and every reified type t satisfies t is Type<T>, where T is the type that t reifies (a concrete proposal is available here).

For example, int is Type<int> would evaluate to true, and so would int is Type<num>, but num is Type<int> and String is Type<num> would evaluate to false. In short, S is Type<T> if an only if S is a subtype of T.

Here is a snippet showing how you'd express the example from this issue:

// Let's assume that `Type{1,2,3}` have a common supertype.
class Type0 {}
class Type1 implements Type0 {
  static Type1 fromJson(Map<String, dynamic> json) => Type1();
}
class Type2 implements Type0 {
  static Type2 fromJson(Map<String, dynamic> json) => Type2();
}
class Type3 implements Type0 {
  static Type3 fromJson(Map<String, dynamic> json) => Type3();
}

// `fromJson` will be an extension method on the type.
extension FromJsonFactory<X extends Type0> on Type<X> {
  static var _fromJsons = {
    Type1: Type1.fromJson,
    Type2: Type2.fromJson,
    Type3: Type3.fromJson,
  };

  X fromJson(Map<String, dynamic> json) {
    var fromJsonFunction = _fromJsons[this];
    if (fromJsonFunction == null) {
      throw UnimplementedError(
        "FromJsonFactory is undefined for the type $this",
      );
    }
    return fromJsonFunction(json) as X;
  }
}

void main() {
  print(Type1.fromJson({}));
  print(Type2.fromJson({}));
}

So how does this compare to the request for static interfaces?

  • There is no notion of a static interface, so the compiler will not insist that any particular class has any particular static methods, and it's just a matter of developer discipline that Type1 .. Type3 all have a fromJson method.
  • However, we're building the lookup table as FromJsonFactory._fromJsons, so we're in control: If we wish to make these static methods from those types available then we're free to do so.
  • Now we can write a fromJson extension method on the type that dispatches to all those other fromJson static methods and returns an instance of the type: T.fromJson(...) returns an object of type T.

For this particular purpose it would be even more convenient if we had those static interfaces, and that would provide a guarantee that every subtype of StaticInterface<T> has some specific members with specific signatures. But that would also give rise to a rather heavy set of constraints on the Dart world of software as a whole, so it's not obvious that it would be realistic (or even beneficial) to introduce static interfaces.

In any case, the above example shows that we can consider other mechanisms to obtain a similar functionality, and they can have very different trade-offs.

@avdosev
Copy link
Author

avdosev commented Feb 3, 2022

@eernstg
The main problem in your example is:

static var _fromJsons = {
    Type1: Type1.fromJson,
    Type2: Type2.fromJson,
    Type3: Type3.fromJson,
 };

In big apps, this map will be large. The language should reduce the number of possible mistakes of the user. Сhecking the signature at compile time would help avoid boilerplate code.

In cpp I can write this code and it will work:

#include <iostream>

using namespace std;

class A {
public:
    static string foo() {
        return "class A";
    }
};

class B {
public:
    static string foo() {
        return "class B";
    }
};

template<class T>
void print() {
    cout << T::foo() << endl;
}

int main() {
    print<A>(); // >> class A
    print<B>(); // >> class B
}

This is what I want in dart.

@Levi-Lesches
Copy link

@eernstg, that can be written today without Type<X>:

typedef Constructor<T> = T Function(Map<String, dynamic>);

class Type0 {
  static const Map<Type, Constructor<Type0>> _fromJsons = {
    Type1: Type1.fromJson,
    Type2: Type2.fromJson,
    Type3: Type3.fromJson,
  };
  
  static T fromJson<T extends Type0>(Map<String, dynamic> json) => 
    _fromJsons[T]!(json) as T;
}

class Type1 implements Type0 {
  static Type1 fromJson(Map<String, dynamic> json) => Type1();
  void test1() => print("Worked");
}
class Type2 implements Type0 {
  static Type2 fromJson(Map<String, dynamic> json) => Type2();
  void test2() => print("Worked v2");
}
class Type3 implements Type0 {
  static Type3 fromJson(Map<String, dynamic> json) => Type3();
}

void main() {
  Map<String, dynamic> json = {};
  Type1 result = Type0.fromJson(json);
  result.test1();
  Type2 result2 = Type0.fromJson(json);
  result2.test2();
}

This is what I want in dart.

Something like this, from #356:

abstract class A {
  static String foo();
}

class B static extends A {
  static String foo() => "class A";
}

class C static extends A {
  static String foo() => "class B";
}

// Static extends means we can use T directly
void print<T static extends A>() => print(T.foo());

void main() {
  print<A>();  // "class A"
  print<B>();  // "class b"
}

@eernstg
Copy link
Member

eernstg commented Feb 3, 2022

@avdosev wrote:

In big apps, this map will be large.

True, it would be helpful if the compiler could generate those maps (just like the compiler, in some implementations of OO languages, generates a vtable to hold all the pointers to method implementations). Perhaps the notion of static interfaces could provide the language mechanisms needed to do this.

However, it is not obvious how we would define the type relations that we'd need in order to be able to guarantee that a certain type variable denotes a generic class that has the specified set of static members.

// Something like this?
class Type1 static implements StaticInterface<Value> {...}

In that case we'd need a notion of "static subtype of" to go along with "subtype of". Any change to the type structure of the language will require changes to basically everything (just think about how much work it was to introduce nullable types ;-), so it's not obvious to me at all how we could introduce such a concept and make the whole thing work.

In cpp I can write this code and it will work:

This is because C++ templates are, essentially, textual macros. So you expand them and compile the resulting code. This means that there is no requirement that the template declaration in itself makes sense, the meaning of each part of it will only be determined after expansion, and different template instantiations can have corresponding parts that have completely different meanings. C++ concepts have been introduced in order to enable a certain amount of static checking of the template itself, but for things that aren't expressible using concepts you can't promise that any given template instantiation will not have compile-time errors anywhere deep in the body of the template.

Dart generic entities (generic classes, generic methods, etc) are type checked at the declaration, and it is guaranteed that instantiations will not have compile-time errors in the body in any instantiation. You just need to check, for instance, that the type arguments passed to a class satisfy the bounds, and then the resulting entity is guaranteed to be type correct and consistent.

C++ templates are in a sense optimally flexible (which is exactly the reason why your example works in C++), but they can break in very low-level ways. Dart generic entities are a proper abstraction, with known properties that don't break just because you instantiate them. It would be hugely breaking, and (in my opinion) a huge step backwards, to adopt a change that would make Dart generic entities work in the same way as C++ templates.

@eernstg
Copy link
Member

eernstg commented Feb 3, 2022

@Levi-Lesches wrote:

that can be written today

Well, to some extent.

Type1 result = Type0.fromJson(json);

In this snippet, fromJson is invoked on Type0 (all invocations of fromJson would be on Type0), and fromJson takes a type argument that determines the chosen type of object to create. This type is chosen statically by the compiler (in case of inference, as shown in the example) or it is written by the developer.

In the approach I mentioned here, the choice of which class to create an instance of is made by choosing a receiver: Type1.fromJson(json) creates an instance of Type1, and X.fromJson(json) creates an instance of the dynamic value of X (and it is statically enforced that X is a subtype of Type0). This makes the expressions similar to named constructor invocations, except that the 'receiver' can be a type variable, and not just a compile-time constant denotation of a class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems state-duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

4 participants