Open FMorschel opened 1 year ago
This sounds like you want to make a third-party class, DateTime
, implement an interface that it's author didn't add to it.
Let's call that interface injection. (Or declaring a "Trait".)
Let's say you can just do that, just declare DateTime implements Date;
. Then your Date
class is just a normal class. You can also declare
-implements
two class where you haven't written either yourself. (But that's probably going to be a problem, so let's require it to be declared in the same library as the superclass for now. In the subclass library you can just use implements
normally. And it'll be weird if it's not in either.)
What would it mean?
Well, statically we'd have to determine whether it's a sound subclassing, so DateTime
must be a valid implementation of the Date
interface. That's something we can check at the declaration of Date
, because DateTime
is imported, otherwise you couldn't write declare DateTime implements Date;
.
Not a problem, easily checked.
We also need to check that Date
and DateTime
don't implement interfaces that are incompatible with each other, say something like Iterable<num>
and Iterable<int>
.
That's still doable. But hints at a deeper problem.
What if one library declares a superclass which implements Iterable<int>
and another a superclass which implements Iterable<num>
. Independently those superclasses are valid, but if both are considered, the results is inconsistent.
A class cannot (and must not, because soundness depends on it) implement more than one instantiation of the same generic interface.
Or what if we declare that B
is a superclass of A
, B
implements Iterable<int>
and A
doesn't. All is well.
But A
has a subclass C
which implements Iterable<num>
.
Most of our soundness rules depend on knowing all superclasses. OO type systems with subclassing declares a subclass in terms of what it is-a of. Having a way to add a superclass (or more than one) to a class that doesn't know about them, is not something the type system is built for. It means the class doesn't know what itself is.
So, this is not very likely to fly unless we can somehow remove the "only one implementation of an interface" rule, which is unlikely.
Could we just say that the subclass doesn't really implement the super-interface, it's just assignable to it. We just pretend. We allow you to assign a DateTime
to Date
, but we all know that it's not really a Date
.
It's not something we can just do locally. We have to treat Date
specially everywhere, because it might be a Date
, or it might be a DateTime
pretending to be a Date
.
That's more like Rust trait than a Dart interface, so maybe it should be:
trait Date { .... }
declare DateTime implements Date;
and traits are special (and cannot implement interfaces, only other traits, which are less strict about being more than one thing than interfaces are). One extra concept, on top of interfaces. A hard sell.
Or we could make all Dart interfaces act like that. Fat pointers, implementation provided on the side. I think it's at least it should be somehow consistent, but probably not entirely consistent with the current design. So another hard sell.
(Sure, if we had just done traits from the start, everything would be easier.)
But Rust traits are really just unboxed implicit wrapper objects. We could do implicit wrapper objects and forwarding instead.
That's more likely to fit into the Dart model. Instead of making DateTime
be a Date
, we create an implicit wrapper, forwarding methods of Date
to the similar methods of DateTime
, and we apply the wrapper when you assign a DateTime
to Date
.
Creating wrappers with implicit forwarding could be useful anyway. Implicitly wrapping too (if that's not just the implicit constructors feature used on the wrapper class).
Then the only thing we need is a way to get back, so if we try to do an is DateTime
or as DateTime
on the wrapper object, it gets unwrapped.
Then there is no limit to how may "superclasses" we can let an object be wrapped as.
(But no easy solutions, that's for sure.)
Thank you @lrhn for being better at explaining my own request than I was (your comment explains precisely what I meant to suggest). I was unfamiliar with Rust's traits, so if you agree, we could change the name for this issue for better understanding.
About what you said about the wrapper objects, I'm completely guessing here, but is there any way for https://github.com/dart-lang/language/issues/2727 to be used for that?
Rust traits are resolved statically in the case where the receiver type is known. In that case an invocation of a traits method will just be a function call to a piece of code which is known at compile time, and it is known statically that the given receiver is an appropriate receiver for that implementation.
In the case where a reference in Rust has a type which is a trait (so the parameter is declared like myParameter: &dyn MyTrait
), the invocation must be performed based on some device that provides the binding of the implementations at run time. In other words, there must be something like a vtable. Rust does this by using a 'fat pointer' which is basically a tiny object containing two "words" (pointer to receiver, pointer to MyTrait
vtable). This approach ensures that the implementations of methods declared by MyTrait
are appropriate for the given type of receiver, even though there is no information available at the actual trait method call sites about the precise receiver type.
Inline class methods are always resolved statically, and this means that they may be used in a situation which is similar to the former, but they will not work in situations like the latter. That is, if you want run-time dispatch to occur then you need to use an object that actually creates the connection between receiver type and method implementation; this may or may not be called a wrapper object, depending on how it is implemented/modeled.
Just to point out, this could be, depending on how things are done, somewhat related to https://github.com/dart-lang/language/issues/83.
Meaning that either issue could be worked on to fit the other one.
If there was a way of creating a class and using it in the same place as a third-party class (not implementing/extending it), that would solve the use case mentioned in the OP.
Mentioned this issue on https://github.com/dart-lang/language/issues/1612 because of HosseinYousefi's comment:
What if we use abstract interface classes as equivalents to Rust's traits
This issue, where Date only needs a subset of DateTime, could be resolved with extension types.
extension type Date._(DateTime inner) {
const Date(DateTime inner) : inner = inner.copyWith(second: 0, minute: 0, hour: 0);
...
}
or otherwise a normal dart class that does the same, which could then implement Comparable as desired
I do like the idea of rust-like traits though, as they would have a ton of benefits. the problem is, because we have subtyping, and because we have dynamic, we could easily end up using a super-type's implementation of a trait instead of the subtype's.
I feel like this is already demonstratable with extensions.
otherwise, its just another way to talk about mixins, with the singular difference being that declaring our own "trait" allows us to implement it for any class we're aware of, which is something we can do with extensions, but cannot define an interface for.
If we can bring these two concepts together into "traits," it could make the language much more powerful It would make unions unnecessary, as you could simply define a shared trait and implement it for the types you want. It would make serialization much friendlier, as you could define exactly how certain "primitive" types serialize, and make the entire thing fully type-safe.
For instance, a Serializable
trait which must return a Serialized
(which the basic json types would implement, where serialize()
'returns this')
In my opinion, the more language features that just-so-happens to solve multiple problems at once, the better. Rust does this super well.
Sometimes, I find some classes created by some package/core dart (such as
DateTime
which I'll use as an example below), that have everything I need, but some extra methods/parameters that are useless for some specific cases.For example, if I want to have a
Date
class, that only hasyear
,month
andday
parameters and let's saycompareTo
,isAfter
andisBefore
methods.If later I want to create another class that extends it, I'll need to only implement these methods and not
toLocal
andtoUtc
for example, which, in my case wouldn't even make sense, and also, no need to always fill all time fields to 0, since forDate
they wouldn't even exist.But, I would like to have an easier way of using
DateTime
objects where myDate
class goes. I know that I could create an extension method forDateTime
that gives me theDate
or even create aDate
constructor that receives aDateTime
parameter. My only goal here is to ask if this could be made easier.I've seen #884 and #2166 that would maybe handle possible extra parameters/methods (or we could simply make that impossible, but I'm not sure that's easier because we would need to write two classes for solving that).
I thought to create something like:
I'm not sure how we would handle the transition from
DateTime
toDate
if there is a certain processing that needs to be done, (like, for example always transforming ittoUtc
to apply to dates in some specific cases) but I'm sure if you find this proposal interesting we can come up with a way of doing that.