Open HugoKempfer opened 5 years ago
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. ;-)
@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;
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.
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.
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.
First of all, for the idea to work, the type
JsonSerializable
itself must implement thefromJson
constructor, not just declare it abstractly. It's possible (based on the type constraints) to callvar user = User.fromJson({"name": "foo"}); testJson<JsonSerializable>(user); // Calls `JsonSerializable.fromJson`.
The
JsonSerializable
type can obviously implementfromJson
so by throwing anUnsupportedError
, 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:
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:
&
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>();
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) => { ... };
}
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:
T.fromJson
because (maybe) not all subclasses of JsonSerializable
actually has a fromJson
(including JsonSerializable
itself), orT.fromJson
if the current type for T
has no T.fromJson
.Since the goal here is to allow T.fromJson
, we need to either:
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
.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:
extends
relations.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.
Not sure how that would be implemented. It means that every
Copyable
class must have a method that can copy everyCopyable
object. If bothDocument
andDanceStyle
areCopyable
, nothing in the signature prevents me from doingDocument.copy<DanceStyle>(someDanceStyle)
, whichDocument
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.
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?
// 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 });
@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 https://github.com/dotnet/csharplang/issues/4436
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.
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:
I tried to produce a non-working equivalent in Dart: