-
Notifications
You must be signed in to change notification settings - Fork 205
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
Multiple upper bounds #2709
Comments
The problem with multiple upper bounds is that it effectively introduces intersection types. That raises a lot of questions that need to be answered, in a satisfactory and consistent way, before such a feature can be added.
The answer to those questions are very likely going to either imply that the language has intersection types in general, or they'll imply inconsistent and surprising behavior. Or just plain useless behavior. (I don't have the answers. I'd love to see a consistent and useful definition which doesn't imply general intersection types, but I haven't found one myself.) |
Personally I would assume this would behave just like as if This wouldnt be like a Union type, where That is also why I would suggest the syntax |
Disallowing conflicts in interface is a reasonable answer to the first two items. If you know that there exists a subclass implementing both, which has a compatible override of both signatures, then you'll just have to extend that instead. The last two items are tougher, because that's not just what you can do with the object, but about whether intersection types exists outside of type variable bounds. I guess that did answer all my questions, so would that be a valid design?
(One advantage of having a type variable with multiple bounds, instead of a proper intersection type, is that type variables don't have any subtypes (other then |
I'm not very well-versed in language design so pardon my ignorance. In lieu of concrete intersection types, would it be possible to leverage record types for this? Wherein a type variable bound is only used for compile-time checking and the concrete type of the type variable is something like This would seem to address all of the points, although I'm curious where this falls apart and if this violates any soundness in the current type system.
This could effectively be
|
Using a pair of values (possibly optional) solves the problem of assigning two types to one value, by assigning two types to two values. What it loses is having only one value. And that's really the most important part. It's not a great API. If you have to pass an void foo((int?, String?) value) ... Nothing prevents you from passing a pair with two values, or zero, and you will have to call it as You'd be better off by writing a proper union type class: abstract class Union<S, T> {
factory Union.first(S value) = _UnionFirst<S>;
factory Union.second(S value) = _UnionSecond<T>;
S? get first;
T? get second;
}
class _UnionFirst<S> implements Union<S, Never> {
final S first;
_UnionFirst(this.first);
Never get second => throw StateError("No second");
}
class _UnionSecond<T> implements Union<Never, T> {
final T second;
_UnionSecond(this.second);
Never get first => throw StateError("No first");
} Then you can do The intersection type is the same, |
– All getters, setters and methods from the types class C<T extends A & B> {
T t;
void test() {
(t as A).foo(); // OK
(t as B).foo(); // OK
}
}
– Yes, class Qux implements Foo, Bar {
void baz(A | B argument) {
switch (argument) {
A a => _bazA(argument); // OK
B b => _baB(argument); // OK
}
}
void _bazA(A argument) {}
void _bazB(B argument) {}
} Note: when the return types are different, the return type will be an union. Nonetheless, it will still be useful, because of the cast that must happen before the method is called. This behavior should not happen frequently.
– Generic type parameters will be able to receive multiple bounds, then method parameters must be able to do that as well! 😀 The type Foo & Bar foo(Foo & Bar value) { ... }
– Finally, considering |
@Wdestroier |
Of course this is going to sound a bit silly of me, but Java and C# solved this problem somehow. And at least C# also has I also find myself in situations where I would profit greatly from intersection types. Just think about the In essence: Intersection types would be really, really neat. |
This issue is not about intersection types but rather specific generic type restrictions. |
Sorry, I was specifically referring to @lrhn's post.
And I tried to explain why I don't think that
is true. |
@lrhn it can start with the simplest case WITHOUT support for intersecting types. mixin A { bool get a; }
mixin B { bool get b; }
class AB with A,B { bool a; bool b; }
//this function only accepts types that implement both A and B
//where A and B MUST be mixins.
//proposed syntax, can be simplified of course.
void myFunc<T with A with B /*, other generic arguments*/>(T input) { /**/ }
myFunc<AB>() //works!
void myFunc<T with String with int>(T input) { /**/ } //disallowed since String, int are not mixins. this follows the same rules as mixins, so it should be easy to implement. |
That's still an intersection type. We have a type That's the difference between a nominal type which implements two interfaces, and we can point to that class to say how, and the unnamed intersection of the two interfaces, where all we know is that whatever actual type ends up here, it has found a way to implement both interfaces. That's an intersection type. That's what intersection types are. Using mixins makes no difference, it's not a restriction. You can implement (non- But a type variable bounded by an intersection can be possible, without introducing intersection types in general. If we allow a type variable to have multiple bounds, then any member access on that type must work on the combined interface. That's a concept we already have, so that's not a problem. The type variable's type would otherwise be assignable to any bound, and be a subtype of either. We might have to make some restrictions to avoid the bounds implementing different instantiations of the same interface. A type variable cannot be bounded by both Any type which implements both would be a valid type argument to that type parameter, which can either be an actual subtype of both, or another type variable which is a subtype of all the bounds. If we do allow that, then we could potentially also allow multiple intersections due to promotion. But probably not. And you can only promote a value with the type variable type to a subtype of all the bounds. |
I have run into the same issue, and without intersection types, types start to leak to other parts of the code base: abstract interface class CsvSerializable {
// ...
}
abstract interface class Dated {
DateTime get dateTime;
}
// A manual non intersection type:
abstract interface class CsvSerializableDatedComparable<T>
implements CsvSerializable, Dated, Comparable<T> {}
class InterestingClass<T extends CsvSerializableDatedComparable<T>> {
// Has methods which use the above three interfaces.
} |
This is what C# does as far as I understand it. It allows multiple bounds on type parameters, but there's no notion of an interface type. It's just that:
In other words, since the intersection is encapsulated inside a type parameter, which behaves pretty much like a nominal type, it works out without too much complexity. Honestly, in Dart, I think we're closer to already having intersection types than C# is because we already support promoted types. I haven't wanted multiple bounds in Dart very often, but it does come up sometimes. |
What Dart differs from C#, and would therefore have issues with its approach, is mainly in not having overloading. In C#, if one supertype has an In Dart, you can only have one
We will be able to assign a type variable to another type variable type, if one is a bound of the other, which means one can have more supertypes than the other. We can promote a variable with a type variable type. We might want to be able to promote more than once then. void foo<T extends A1 & A2>(T v) {
if (v is B1 && v is B2) {
// Promoted to B1, not B2,
// Or promoted to B1&B2 ?
// The latter would be nice.
}
} Promotion to an intersection introduces structural intersection types, which was what we tried to avoid. |
True, sometimes it's hard to tell if we've really avoided that much complexity in not having overloading. It avoids a lot of problems, but it creates other ones too, like having to synthesize merged members in superinterfaces. |
sometimes I wonder how effective it would be to implement explicit overloading, e.g. two mixins/classes can have two methods with the same name and different signatures, but implementers can separate them mixin A {
void m();
}
mixin B {
int m(String param);
}
class AB with A,B {
void A.m() { /*method to be called only when target class is A*/ }
int B.m(String param) { /*method to be called only when target class is B*/ }
List<String> m() { /*method to be called only when target class is AB*/ }
} which are called via casting: final AB ab = AB();
final a = ab as A;
final b = ab as B;
ab.m() // targets List<String> m()
a.m("") // targets void A.m()
b.m() // targets int B.m(String param) |
That sounds to me sort of like explicit interface implementation in C#. I think if we were to do something like this, we'd also want normal overloading too. The latter is really useful in API design beyond just disambiguating when you have member collisions from superinterfaces. |
I've not yet read the full extent of this issue thread, so if I'm repeating something I apologize, but I'd like to share one use case for this. I'm developing a Flutter app with Drift, a package for dealing with SQLite databases that uses Drift's approach involves creating non-abstract classes that extend In my project, I have several tables with at least two common primary keys (PKs). These tables extend a simple abstract table class containing these columns. In my DAOs, I consistently implement a set of specific, similar methods across these tables. To streamline this, I created a base class with a generic type T that extends the abstract table. However, only the specific classes generated by To ensure compatibility, I specify that My workaround involves creating another abstract class with the dependencies correctly arranged. However, every time I use I also asked for help in the Community Discord, and someone (julemand101) suggested creating my own mixin that meets these constraints. However, I can't do this because I'm asking here something similar to the "extended" classes issue (akin to Rust Traits, as mentioned in the comments). |
Why exactly are we avoiding structural intersection types? It seems to me like a strictly useful feature. It is very useful for mixins, and of course normal promotions. after all, if I do I mean, the language itself already sort of supports it doesn't it? i occasionally see Syntax wise, there arent any operators in use for types, so we could totally use Intersections would allow us to get rid of classes who's only purpose is to combine the unrelated types/mixins for generics. As it is, mixins are... not as useful as they can be since we cant ask for multiple in a "flat" way without some kind of weird wrapper thing. example with intersection types mixin Foo {
Thing foo();
}
mixin Bar {
Other bar(That that);
}
Something run(SomeContext context, Foo & Bar it) {
final Thing thing = it.foo();
final that = context.of(thing);
final Other other = it.bar(that);
return makeSomething(other);
} To do the same thing now, i would have to require two parameters for the same object, which might break the contract if the caller passes different objects in. asserts can catch that, but its a terrible API. For the record, Foo and Bar could have dramatically different usages elsewhere and different interactions with other types right now, i would have to create a: class FooBar implements Foo, Bar {
final Foo _foo;
final Bar _bar;
FooBar(this._foo, this._bar) : assert(identical(foo, bar));
Thing foo() => _foo.foo();
Other bar(That that) => _bar.bar(that);
// incredibly painful for larger interfaces.
}
Something run(SomeContext context, FooBar it) {...}
// perhaps a need for an extension so we could do `foo.with(bar)` which has the result of doing `it.with(it)` if users don't know about the interface, or cant implement it. which is a terrible API and does not scale.
AND! Any future functions that want any combinations later cant just ask for the relevant interfaces. so either do runtime checks, or use combinator-forwarding classes what we wouldnt need if the language had supported it. I have had to just... avoid mixins and such apis entirely because you just cant make this work at all right now. |
Structural intersection types come with a usability issue: what is the interface of the type? And can it be reified as a type separate from the type variable bound. Assignability from an intersection type is fine. It's assignable to both types. If both types declare a If one has an optional named The options are:
In the latter case, failing to have a signature can be avoided by giving the parameters union types. But then we introduce union types to the language too, and those come with even more usability issues, especially if they can be introduced by type inference (fx you won't get a type error as easily). Overstuffing general intersection types usually implies obtrusive general union types. Which is a very complex thing to do to your type system. The current intersection types in the language are always intersecting with a type parameter and a type that is a subtype of the bound of that type variable. If we have general intersection types, not just multiple bounds on a type variable, you can also have a variable that has an intersection type as declared type. Then assignability to the type starts to matter, and testing against the type. What does How does a typedef typeof<T> = T;
void main() {
var dan = typeof<dynamic & num>;
print(dan == num); // True or false?
print(dan == typeof<Object? & num>); // True or false?
print(dan == typeof<void & num>); // True or false?
} The static type system and the dynamic type system both need to address these questions. |
Where can we have a failed signature? I don't know of any case where dart allows you to have conflicting types of the same member name anywhere. By definition, any member would have to be that does mean that all optional parameters need to be included, as any object implementing both with have them. which would be part of the rules for intersecting function types also, any intersection where either is a supertype of the other, can be normalized to be the subtype, as it by definition implements the other already. that means Any intersection that we can determine is impossible (such as a final class and a non-supertype (which (the supertype) is normalized away anyway)) is effectively
|
If we do want to avoid general type intersections, at least for this issue, we could keep it super simple. We don't intersect. Instead, it's about as useful as existing union types on their own - that is, it isn't. It then becomes mostly an API thing, and authors using the feature can freely assign to specific values like: void fn<T extends A&B>(T val) {
final A a = val;
final B b = val;
...
} Which is entirely free to do. T only receives the interface if it's upper bounds, which just reuses existing implementation. In effect, we get discount intersection types, only it's in such a way that we can add more to it later for real intersection types. A step up; we only intersect trivial members, like those that don't exist in both, and add rules for intersection over time, slowly refining it, instead of needing to do the whole thing all at once. In effect, give nothing but the code of this issue, and expand it's capabilities and limits as we figure them out. I'm not sure if we can do the same thing with union types... Something to think about later I suppose. My opinion is that we should introduce intersection types and untagged union types both, but there's no reason we can't take a baby step in that direction. |
I have a problem understanding what But to implement A and B, class C has to provide implementations of all methods of A and B. If any signature conflict arises between methods, then C is responsible for resolving it, which it sometimes can, or sometimes cannot do, like in this example class A {
String toPrettyString() => 'A';
}
class B {
String toPrettyString(int x) => 'B$x';
}
class C implements A,B {
String toPrettyString(int x)=> 'C'; // error: ... is not a valid override of ...
} It's impossible for C to implement both A and B. This eliminates one of the problems: "what happens if the parameter is declared with a type A&B, and there's a conflict of names? Which method to call?". This problem doesn't exist. Or maybe (@lrhn: I'm getting a strange error message in the above program. I can't copy from dartpad (a bug?), but you will see it) |
That Some intersection types are always going to be empty (meaning they contain no instances, not that they contain no subtypes, they likely contain lots of subtypes all the way down to The example here can be solved:
An incompatible example would be class A {
int get x = 42;
}
class B {
int x() => 42;
}
void foo<T extends A&B>(T value) {
... value.x ... // Can't touch this!
} There will never be a concrete subtype of both Another example is The type system has no problem with the type The error message for your example (you can copy it if you right-click and avoid the browser context menu covering the "copy text" option) is:
That's correct, it's not. |
I stand corrected about
To counter this, you have to provide an example of the class C that correctly implements both A and B defined as class A {
int get x = 42;
}
class B {
int x() => 42;
} Does such an implementation exist?
I don't understand where 'String Function()' comes from, sorry. I'm sure there's a reason for this, but I expected to see something simpler: |
class A {
int get x = 42;
}
class B {
int x() => 42;
} There is no way to implement both. It is not possible to have a member that's both a method and a getter. The member is |
If the compiler can determine that some method cannot be implemented by both A and B, then the type For the sake of an argument, let's suppose that the compiler doesn't complain in this scenario: class A {
int get x = 42;
}
class B {
int x() => 42;
}
void foo<T extends A&B>(T value) {
// NO COMPLAINT unless you use value.x
} Now suppose the caller calls this method passing a parameter of type C class C {
//...
}
foo(C()); No matter how class C is defined, the compiler cannot allow such a call, because for sure, the parameter is not compatible with the type |
Correct, but The same thing happens for Unless the compiler guarantees that no valid argument can ever reach I think it's easier to just do the analysis, and bail out if an incompatibility is found which would preclude continuing, like reading That is: Don't bother detecting if a type is empty or not. It's not worth it. |
I think I get it. Please verify that my understanding is correct. class A {
String toPrettyString() => 'A';
}
class B {
String toPrettyString(int x) => 'B$x';
}
foo(A & B arg) {
arg.toPrettyString(); // works
arg.toPrettyString(10); // works, too
} In other words, while compiling the code for Then the question: is there any difference between |
Well You can't simply do foo(A | B arg) {
arg.toPrettyString(); // error
arg.toPrettyString(10); // error
} Because if In order to call the method from foo(A | B arg) {
if (arg is A) {
arg.toPrettyString(); // works
}
if (arg is B) {
arg.toPrettyString(10); // works
}
} This is similar to how null safety works, a variable of type |
Oh, my bad 😄 class A {
String toPrettyString(Foo foo, Bar bar) => 'A';
}
class B {
String toPrettyString(int x) => 'B$x';
}
foo(A & B arg) {
arg.toPrettyString(Foo(), Bar()); // fine
arg.toPrettyString(10); // fine, too
} |
Well if you think of |
Exactly. The compiler has to compute a "common denominator" or these two signatures. Not sure about "Never" - if it comes to Never, it's probably better to flag the |
I'm not sure what you mean about common denominator. My understanding is that if you call
What I meant was |
The compiler has to verify that the call makes sense statically. It has to reject (statically!) the call that has no chance of being both A's foo and B's foo. For this, it has to be able to derive the signature of the "common denominator" - that is, the method that satisfies both foos. Imagine you are writing the code of |
Okay I think this is where our understanding differs. The type
I'd expect to see both method signatures suggested. |
No. It can't suggest class A {
String toPrettyString() => 'A';
}
class B {
String toPrettyString(int x) => 'B$x';
} With another definition (with (Foo, Bar) in one place and (int) in another)... I don't know... probably again it's @lrhn: please help! 😄 |
What @mmcdon20 says. The analyzer can just show both types. That's the most correct answer. If you have class A {
String foo(A a) => '...';
}
class B {
String foo(B x, B y) => '...';
} and something of type (Ignore whenever the argument types are final types, that's something one can special-case, but it shouldn't be necessary. Ignore what the existing method implementations are, a type which is both an At a minimum, it should allow both Maybe it can be called as The member signature can be represented as the intersection type If the function types are incompatible, like |
That's an interesting idea. If this were the case, could that also potentially allow method overloading in dart? For example class A {
int foo(int a, int b) => a + b;
String foo(String msg) => 'hello $foo';
}
void main() {
final f = A().foo; // int Function(int, int) & String Function(String);
} |
@lrhn wrote:
I know, there's a strong argument for this: if A & B is feasible, then we can use either signature by definition. class A {
int get x = 42;
}
class B {
int x() => 42;
}
void foo<T extends A&B>(T value) {
... value.x ... // Can't touch this!
} What do you mean by "don't touch it"? You just said that IDE can suggest both signatures. That is, I can write void foo<T extends A&B>(T value) {
value.x; // using suggestion #1
value.x(); // using suggestion #2
} I've just touched them! The funny thing is that it absolutely doesn't matter whether I touched them or not - because the function class C implements A, B {
// impossible to define it satisfying both A and B
} So no matter what parameter I try to pass while calling the method (like |
My point there is that with two different methods signatures, the compiler knows that any object implementing both will have a method with a signature that is a subtype of both. It doesn't know which signature, but it can deduce some ways that it can be validly called. With one getter and one method, the compiler has no idea where to start. Intersection types are for types. So if you do write |
An argument can be made that if A and B contain a method Consider a method foo(A & B value) {
var arg = something;
value.x(arg); // suppose the compiler allows the call
(value as A).x(arg); // then these should be allowed, too
(value as B).x(arg);
} Why the last two calls should be allowed? Because all the compiler knows about the value is that implements both A and B. |
One of the last two calls would result in a compile error. For comparison see the equivalent program in typescript. class A {
x(s: String) {}
}
class B {
x(x: number) {}
}
function foo(value: A & B) {
var arg = 3;
value.x(arg);
(value as A).x(arg); // Argument of type 'number' is not assignable to parameter of type 'String'.
(value as B).x(arg);
} The above program produces a compile error at the commented line. |
Could you check the suggestion shown by IDE when you type |
@tatumizer you can try yourself on https://www.typescriptlang.org/play The editor shows both with a
The compiler appears fine with that. |
I see. X is (A & B) iff A is X AND B is X. |
No it should be iff X is A AND X is B. That said I'm not sure why However If you try the same thing in Scala you do get an error. Scala example below: trait A {
def x(s: String): Unit
}
trait B {
def x(s: Int): Unit
}
def foo(value: A & B) = {
value.x(Object()) // None of the overloaded alternatives of method x in trait A with types
// (s: String): Unit
// (s: Int): Unit
// match arguments (Object)
} Should also note that since Scala supports overloading there is no conflict between the two variations of trait A {
def x(s: String): Unit
}
trait B {
def x(x: Int): Unit
}
class C extends A with B {
def x(s: String): Unit = {}
def x(x: Int): Unit = {}
}
def foo(value: A & B) = {
value.x("A") // legal
value.x(3) // legal
}
foo(C()) // legal EDITActually you do get an error when passing in class A {
x(s: String) {}
}
class B {
x(x: number) {}
}
function foo(value: A & B) {
value.x(new Object());
// No overload matches this call.
// Overload 1 of 2, '(s: String): void', gave the following error.
// Argument of type 'Object' is not assignable to parameter of type 'String'.
// The 'Object' type is assignable to very few other types. Did you mean to use the 'any' type instead?
// Type 'Object' is missing the following properties from type 'String': charAt, charCodeAt, concat, indexOf, and 37 more.
// Overload 2 of 2, '(x: number): void', gave the following error.
// Argument of type 'Object' is not assignable to parameter of type 'number'.(2769)
} |
The bottom line: the issue is moot 😄 |
@mmcdon20 is right.
That's correct. The casts are up-casts. Say we had class C implements A, B {
void x(OIbject _) {}
} which was the actual runtime type of (I'm JavaScript, I think |
I understand this. But I find both the concept and the notation confusing. class A {
int foo(String s) => 0;
}
class B {
int foo(int i) => 0;
}
class C implements A,B {
int foo(Object o) => 0; // yes, this is correct override
} Now let's consider a function func(A a)=>a.foo(Object()); // ERROR! The argument type 'Object' can't be assigned to the parameter type 'String'. So far so good. Now let's change func(A & B a)=>a.foo(Object()); // no error Again, I understand the formalizm that leads to such behavior. But I find it bizarre. On the surface, it looks like we are tightening the type by imposing an additional requirement. But in reality, we remove all type restrictions. |
That's because you don't expand to It just so happens that the actual class would need to accept Object, but we don't actually know that in the function. |
I need to implement a solution using generics that implements 2 interfaces, but as far as I can tell, generics in dart only supports 1 upper bound?
The contents of the 2 interfaces is not really relevant, and what I'm trying to do, is construct a class that can process this in generic form.
What I want is something like this:
I know this is possible in both Typescript and Java, but I'm fairly new at Dart. Anyone know?
The text was updated successfully, but these errors were encountered: