-
Notifications
You must be signed in to change notification settings - Fork 213
Allow some kind of structural typing #1612
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
Comments
Indeed but there are no problems because they both describe the same shape of object.
I my mind |
Duck typing would be equivalent to If we take How would subtyping work otherwise? I'd expect:
From 1., 3. and transitivity we get that It would introduce yet another top type since So, what about the special types in the Dart type system (we have quite a lot of those!)
So, all in all, we can just disallow all special types and require that (This feature differs from the idea of having |
@tatumizer Very good questions. I'd personally disallow Probably won't allow If we treat I don't see self-types coming from this approach, not any easier than what we have today, so |
It would be more natural (but more complicated to implement, I'm sure) if the type system allowed checking for this when all else fails. In other words, if abstract class Addable<T> {
T operator +(T other);
}
T add<T extends Addable<T>>(T a, T b) => a + b;
void main() {
/// Strings are `Addable<String>` since [String.+] is defined as `String +(String other)`
String one = "Hello";
Addable<String> two = "World"; // both declarations are valid
String three = add(one, two); // T is String
} |
We have several data structures in our app that could work with anything that has a length and is indexable. However, in these cases, we're forced to declare the type signature as a very broad There may be a risk of misusing the interface, but having worked in languages with structural typing, I think the benefits of less boilerplate and more flexibility outweigh the risks of passing in the wrong thing. |
Dart doesn't check the body of methods for type information, that's all in the signature. Accessing members on an object in code and telling the compiler "this is okay" seems like a good use for |
Still doesn't answer the question -- we don't usually plan to make errors in our code, so a keyword saying "this code has no errors" isn't that accurate. What's wrong with abstract classes? Abstract classes are meant to specify "here's what I want a type to have, and here's what I want to call that type". In your code, you want Dart to define an interface with |
As far as developer UX, my preference would be for the syntax to be similar to abstract classes since we already use them as interfaces ( The main differences I am aware of are:
|
That's fine though, since you can (and often do) have abstract classes with only abstract methods
That's why I would suggest that the type system automatically check if a class could implement an abstract class if all else fails. So |
@Levi-Lesches It doesn't even have to be a (big) breaking change. If the interface was not intended for anyone else to implement, then changing a method from If someone explicitly implemented the interface, even if they weren't supposed to, then they'd be broken, but would have only themselves to blame. If a class was made to implicitly implement the interface, just because it had an We'd need to have away to declare interfaces either "open" or "closed", and only implicitly implement "open" interfaces (with the default being "closed"). Being an open interface would be a promise to users. |
Right, this is definitely a fragile system. What if the onus was on the client, not the original interface? Some keyword like |
Little case for checking abstract class Collection<T> {
int get length;
T operator [](int index);
}
bool isCollection(dynamic value) {
if (value is Collection) {
return true;
}
try {
value.length;
value[0];
return true;
} on RangeError {
return true;
} catch (_) {
return false;
}
} Also #736 . |
@lrhn to make sure I understand this right, how would it look from a developer experience perspective? I suppose you mean these situations?
Does it come down to error noise? Or am I missing some other considerations? And I suppose this is less of a problem within the same codebase (easy to fix the errors), but once we start thinking cross-package--where you don't know who depends on you--it becomes more problematic. What if it was possible for a developer to explicitly implement an interface but it didn't have to be in the class declaration itself? So I could define it like And I believe Typescript interfaces are structurally typed. I wonder how it's handled with them.
@Levi-Lesches I think we're in agreement. I just meant that's as a small difference. So it feels like abstract classes are the closest concept to interfaces that we have today. |
After giving it more thought, I think
@lrhn made the point that
And they're both correct. We don't have any guarantee that And even if we could force the types to implement some common interface, that wouldn't be enough. To borrow from @eernstg in #281:
In other words, we seem to all agree that this is inherently unsafe, and neither the API creator nor user can make this decision on their own and call it safe. IMO, |
Another use case to share. We have a sort of hacked implicit interface implements in part of our app. We generate types from a GraphQL schema and we want it to be possible to use some of the objects interchangeably. For example a User with an id, name, photo should be allowed to be used in place of an id, name because one is a superset of the other. So if someone fetches more data than is necessary in a query, they are still allowed to use the response in widgets/functions that need less info. So we end up with some long class User
implements
AuthUser,
BasicUser,
BasicUserWithTimezone,
DocumentToken,
FeedSource,
GetChatsCurrentUser,
GetCurrentUserProjectsCurrentUser,
GetUnreadBadgesCurrentUser,
ManagementUserWithProfile Also class AuthUser implements BasicUser We loop over all the fields to figure out who could implement what and generate the implements based on that. This is because our widgets don't necessarily care about the name of the class that's being passed in. We have control over the code-generation so we have the luxury to do this. The benefit is we get compile time type checking. We pass in a class that the fields and types don't match up, and we get an instant error in our editor. As a similar thought experiment, suppose I wrote the following: @Implicit()
abstract class HasLength {
int get length;
} Then, before compiling the app, I rewrote all the classes in the (Not suggesting this is the solution, but posing an idea to understand trade-offs better.) What would be the main downsides of that in terms of the Dart compiler and developer experience? |
As an alternative - what about having something like For the length one, it'd be really nice to have something like |
IOW - I'd find it really hard to reason about calling a method that just wants my type to implement some method without knowing why, but I'd find it really pleasant to label my type as having a method you can use with a well defined contract. |
plus for EfficientCountable |
This seems orthogonal to this issue IMHO. Imagine we have a At the end of the day we will always have the |
Won't that do lots of nasty things to tree shaking though? For example, once the compiler sees that you dynamically call You could always decorate your unmarked type, although that does add some work/overhead. |
@dnfield I like the idea to have more granular interfaces introduced to the dart data structures. However, I think this wouldn't solve the broader issue. This would help the narrow use case of built-in data structures, but it would be hard for the Dart/Flutter teams to anticipate all of the use cases that would arise. Similarly, any package author would have to anticipate all of the possible usages of a class and create a bunch of interfaces trying to account for all of these possibilities.
I agree with this. Part of the problem is that, if a class hasn't explicitly implemented an interface, I have to resort to dynamic or wrap that object if I want to get the benefits of static typing. I'd want to allow to declare an interface after-the-fact. Here I'd be able to give it a proper name. I acknowledge there is the downside where you may incorrectly pass in an object, but that is no worse than the There are ways to do this such as wrapping the types and having the wrapper explicitly implement the interface, but the amount of boilerplate required leads me to reach for the lower friction |
this could help with serializing packages (which is increasing the amount each day along with state management packages). dynamic<JsonObjEncodable> foo;
class JsonObjEncodable {
Map<String, dynamic> toJson();
} |
@lrhn , I would probably suggest considering this syntax. bool hasEvenLength<T implements HasLength>(T object) => object.length.isEven;
|
Is that what this quote said? class Foo implements dynamic<HasLength> {
...
} But my version of the syntax is about something else. In this variant we say that the generic T type should implement HasLength without forcing the T type to have a v-table. This variant may remove some of the problems mentioned above (T isn't dynamic) That said, commenting on the need for structural typing seems pointless, there are situations where we cannot influence external code and it is hard to do anything about it. |
What if we use abstract interface class Countable {
int get length;
}
abstract interface class HasIsEmpty {
bool get isEmpty;
}
class EmptyArray {
final int length = 0;
}
extension E on EmptyArray implements Countable, HasIsEmpty {
// No need to add the [length] getter, as [EmptyArray] already has it.
// However, we need to add [isEmpty].
bool get isEmpty => length == 0;
}
void f(Countable c) => print(c.length);
void main() {
f(EmptyArray()); // prints "0"
} |
Hi! Found this issue today. As mentioned above by HosseinYousefi:
I had created an issue for that without knowing that Rust had this implemented and what name it was: #3024. Just commenting here so we can have all the links. Edit:Also potentially related to: #2166 |
In several classes you can find methods with the same signature but those classes usually don't share a common interface defining this methods. For instance
int get length
is available on String, Ìterable, and Map.If you define a function that only need as parameter an object with a
int get length
you have to usedynamic
for this parameter. For instance:Today there is no way to express this contraint : the parameter object needs to have a length getter returning an int.
A straw-man solution could be to allow
dynamic
to be parametrized with a type that the object should conform to. Thusdynamic<HasLength>
would mean a class that would be valid if it would implement HasLength :Reusing
dynamic
makes some sense as it's still a dynamic call but on something that has a known shape.Another solution without langage enhancement could be to have an annotation to express the shape of object that is expected and let the analyzer trigger errors when the provided type is not conform. This solution would be less powerful as the shape will not be carry by the type.
The text was updated successfully, but these errors were encountered: