-
Notifications
You must be signed in to change notification settings - Fork 209
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
Comments
@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(...)
} |
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 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. |
@avdosev, over a long period of time we have discussed a particular language feature whereby the built-in type For example, 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?
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 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. |
@eernstg 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. |
@eernstg, that can be written today without 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();
}
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"
} |
@avdosev wrote:
True, it would be helpful if the compiler could generate those maps (just like the compiler, in some implementations of OO languages, generates a 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.
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. |
@Levi-Lesches wrote:
Well, to some extent. Type1 result = Type0.fromJson(json); In this snippet, In the approach I mentioned here, the choice of which class to create an instance of is made by choosing a receiver: |
Introduction
Sometimes there are situations when it would be great to write something like this:
Where static interface is:
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:
With static interfaces this could be much shorter:
Project with same problems
Hive - they use Hive.registerAdapter but could use this feature
more about registerAdapter
The text was updated successfully, but these errors were encountered: