Open
Description
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:
abstract class RawRepresentable<T> {
T get rawValue;
}
extension EnumByRawValue<T, E extends Enum & RawRepresentable<T>> on E {
// compile error
}
I know this is possible in both Typescript and Java, but I'm fairly new at Dart. Anyone know?
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
lrhn commentedon Dec 11, 2022
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.
T foo<T extends Foo & Bar>(T value) { ... }
, what can I do tovalue
inside the body?Foo
andBar
declare a method namedbaz
, can I call it? With which signature?var z = foo(something as dynamic);
, what will the implicit downcast fromdynamic
be to?z
?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.)
clragon commentedon Dec 11, 2022
Personally I would assume this would behave just like as if
T
was a class that implemented bothFoo
andBar
in some way.Those conflicts are then handled the exact same way as they would inside of
T
.if two methods have the same name but a different signature, its simply not possible to implement them both.
T
would make all properties of bothFoo
andBar
available. They cannot conflict as that would be a compile time error.This wouldnt be like a Union type, where
T
can be eitherFoo
andBar
, it has to be both just like if we were to make a real class that is bothFoo
andBar
.That is also why I would suggest the syntax
T extends Foo extends Bar
instead of&
.We might want to implement actual union types at some point, which then would be more prone to using
&
and|
.lrhn commentedon Dec 11, 2022
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.
We can use the same rules for when interfaces are compatible that we do for classes inheriting multiple interfaces, and not having an override themselves.
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.
If they do, then the language just has intersection types. It's no longer about type parameter bounds.
If not, ... what is happening in
var z = foo(something as dynamic);
? Will the type ofz
beFoo
,Bar
ordynamic
? Which type check(s) will happen on the argument tofoo
?It's incredibly hard to constrain something to just type variables, because type variables also occur as types in the inputs and outputs of the thing that declares the variable. They leak. Even if every instantiation is guaranteed to have a concrete type, type inference only works if we can find that type.
(So an answer could be that
var z = foo(something as dynamic);
fails to compile because we cannot infer a concrete type argument tofoo
, and intersection bounds cannot be instantiated-to-bound.)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
Never
). One less thing to worry about.)dnys1 commentedon Dec 15, 2022
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
T = (Foo, Bar)
forT extends Foo & Bar
andT = (Foo?, Bar?)
forT extends Foo | Bar
?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.
value
would have type(Foo, Bar)
and would need to be de-structured before use.baz
could be called independently on each or restricted, as mentioned above, such that it is a compile-time error.This could effectively be
var z = foo((something as Foo, something as Bar));
z
would have type(Foo, Bar)
lrhn commentedon Dec 15, 2022
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
int | String
to a function, it's signature would be:Nothing prevents you from passing a pair with two values, or zero, and you will have to call it as
foo((null, "a"))
orfoo((1, null))
. Not that good ergonomics.You'd be better off by writing a proper union type class:
Then you can do
void foo(Union<int, String> value) ...
and call it asfoo(Union.first(1))
.The intersection type is the same,
foo((List<int>, Queue<int>) listQueue) ...
, where nothing ensures that you pass the same value as both pair-values. Calling it will befoo((listQueue, listQueue))
.Wdestroier commentedon Dec 16, 2022
– All getters, setters and methods from the types
Foo
orBar
can be directly called from the method body, with a single exception. The single exception refers to methods, getters and setters with the same name, but different return types. To call these methods the class must first be casted toFoo
orBar
to avoid intersecting the return types (int & String
isNever
) The return type is not useful when it's typed asNever
. Also, the class can be casted toFoo & Baz
, butBaz
must not have the name conflict mentioned above (more on this after the next topic).– Yes,
baz
can be called with an union of both signatures. Methods with different parameters, but the same name, must receive a union of these types and then handle the type in the method body. The IDE must show an error if theQux
class doesn't implementvoid baz(A | B argument)
. The implementation of thebaz
method is up to who's writing the class.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
T
inT foo<T extends Foo & Bar>(T value) { ... }
must beFoo & Bar
unless better inferred by the type system asQux
.foo
can be represented without generics as following:– Finally, considering
foo
returnsFoo & Bar
, the declared type ofz
infinal z = foo(argument)
must beFoo & Bar
.lrhn commentedon Dec 17, 2022
@Wdestroier
This is a perfectly good description of actual intersection and union types (which is issue #1222 ). If Dart had those, this issue would not be needed.
What is being explored here is whether it could be possible to have multiple bounds on a type variable, without introducing general intersection types - and union types, because those two go hand-in-hand.
(That was also mentioned in #1152, as part of a more complex discussion, and got lost. Which should be a lesson about not rising multiple problems in the same issue.)
MarvinHannott commentedon Sep 11, 2023
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
dynamic
. So from my point of view, this isn't completely new territory. Is there a particular reason why this is hard to achieve in Dart?I also find myself in situations where I would profit greatly from intersection types. Just think about the
Iterable
interface and how inflexible it is. For example, conceptually an iterable might know its own length. But it is exactly this "might" that can't be expressed in Dart. So either thelength
property gets implemented inefficiently or it just throws at runtime. Or you implement all possible combinations of interfaces (Iterable
could have many more optional features likeBidirectional
) as their own interface (effectively the power set), which would be insane. In C# I can express this easily.In essence: Intersection types would be really, really neat.
clragon commentedon Sep 11, 2023
This issue is not about intersection types but rather specific generic type restrictions.
You might be looking for #83.
MarvinHannott commentedon Sep 11, 2023
Sorry, I was specifically referring to @lrhn's post.
And I tried to explain why I don't think that
is true.
37 remaining items
mmcdon20 commentedon Nov 5, 2024
One of the last two calls would result in a compile error. For comparison see the equivalent program in typescript.
The above program produces a compile error at the commented line.
mmcdon20 commentedon Nov 5, 2024
@tatumizer you can try yourself on https://www.typescriptlang.org/play
The editor shows both with a
1/2 x(s: String): void
and2/2 x(x: number): void
with arrows to switch between1/2
and2/2
.The compiler appears fine with that.
mmcdon20 commentedon Nov 5, 2024
No it should be iff X is A AND X is B. That said I'm not sure why
value.x(Object())
is allowed by typescript, but it could just be that typescript is not entirely sound (which it 100% isn't). It would make sense to me ifObject()
was a bottom type in typescript but as far as I know it is not.However If you try the same thing in Scala you do get an error.
Scala example below:
Should also note that since Scala supports overloading there is no conflict between the two variations of
x
.EDIT
Actually you do get an error when passing in
new Object()
in typescript as opposed toObject()
. I'm not quite sure what is going on with the latter but you can't pass an instance ofObject
without getting an error.lrhn commentedon Nov 6, 2024
@mmcdon20 is right.
That's correct. The casts are up-casts.
An up-cast can change a parameter to be more restrictive.
Say we had
which was the actual runtime type of
value
. Then the errors make sense.And so would they if the static type of value was
C
instead ofA&B
, unsurprisingly sinceC
<:A&B
.(I'm JavaScript, I think
Object()
withoutnew
is a coercion of the argument to be a non-primitive value. Without an argument, that's a coercion ofundefined
. I'm guessing its value isnull
. TypeScript might know that, and make the static typeNull
or the TS equivalent.)TekExplorer commentedon Nov 6, 2024
That's because you don't expand to
Object
you expand toint|String
which means you can pass either an int or a String.It just so happens that the actual class would need to accept Object, but we don't actually know that in the function.
async
annotation dart-lang/native#1779T
to extend multiple classes #4195jodinathan commentedon Jan 16, 2025
My simple mind thought this would be like a syntax sugar, ie:
then