Open eernstg opened 3 months ago
Without Unit
being sealed, it doesn't make much sense, since I can always declare abstract class MyUnit extends Unit {}
and use Length<MyUnit>
which will still not work.
I'd assume that T strict extends Super
is a (proper) subtype of Super
, so that's at least neat.
Are proper-subtype-bounding only valuable for type parameters, or could I have a normal variable with a proper-subtyped type? (Or is that just useless if the excluded type is abstract, and meaningless if not?)
Maybe this should only be about sealed types.
Consider <T extends sealed Super>
which requires the passed type to be one of the immediate supertypes of the sealed type Super
. Or if any of those types are themselves sealed, then their transitive first non-sealed subtypes. "Don't mention the abstracts!".
Or could it be <T extends concrete Super>
which requires T
to be a concrete type. T extends concrete Super
is then a proper subtype of Super
, and X extends Sub
(where class Sub extends Super
is concrete) would not be a valid argument for T extends concrete Super
, because it might be a non-concrete subtype.
(Only allowing concrete classes came up in the virtual static method discussion, because you can't call a generative constructor on a type variable, no matter what subtype requirements it satisfies, if the class is abstract.)
It's a new kind of type bound, which means that the bounded type variable is a new kind of type, with either approach.
If I have foo<T strict extends Super>(T value) { if (value is SubSub) { ... } }
will it promote value
to T&SubSub
? (It can, and as an intersection type, it's valid, even if SubSub
is not a an immediate subtype of Super
, or even a concrete class.
use
Length<MyUnit>
which will still not work.
I don't see a problem: MyUnit
will be considered to be a proper unit. We might not have heard about before, but otherwise it is just as good as Meter
.
import 'unit_with_meter_and_foot.dart';
abstract class MyUnit extends Unit {}
extension on double {
Length<U> toLength<U strictly extends Unit>() => Length<U>(this);
}
void main() {
Length<MyUnit> l1 = 2.5.toLength(), l2 = 4.5.toLength();
Length<Meter> l3 = 5.5.toLength();
var sum = add(l1, l2); // OK, result is `Length<MyUnit>`.
add(l1, l3); // Compile-time error.
}
Granted, we might wish to have something which is more flexible and has more expressive power, such that we would be able to make MyUnit
an "abstract type argument" as well, rather than an actual unit. That's not easily achieved using anything like strictly extends
.
However, a more powerful mechanism might also be a lot more expensive in terms of language and implementation complexity, and we might not want this expressive power so badly that we want to pay that much to get it. ;-)
or could I have a normal variable with a proper-subtyped type?
I think abstract
would be the best fit for that concept.
Maybe this should only be about sealed types.
I think sealed types are the simple case that we don't have to cover, because they can already be handled in a manner which is similar to this proposal. It might still be convenient (and more maintainable) to use this feature with a sealed type, it's just not essential. It is essential with types that are not sealed, though, because we can't enumerate the acceptable types and hence we can't provide for exactly that set of types.
This is also the reason why Unit
wasn't sealed: It should be possible to declare additional unit types, for anyone who wants to do so.
Or could it be
<T extends concrete Super>
which requiresT
to be a concrete type.
Would that be useful? It sounds like a restriction which is somehow similar to abstract
, so maybe we could use concreteness in some way to emulate the behavior that we'd want to use T extends concrete Super
for.
In any case, it certainly wouldn't allow us to express anything like "every type which is a subtype of Object
, but not Object
itself".
If I have
foo<T strict extends Super>(T value) { if (value is SubSub) { ... } }
will it promote value toT&SubSub
?
Certainly yes, if value
is otherwise promotable, and Subsub
is a proper subtype of Super
(it does not matter whether it's direct or indirect, and it would presumably be OK to promote value
to T & Super
as well).
It could be claimed that we can use existing mechanisms. It is indeed possible to take some steps:
sealed class Unit {}
sealed class _GhostUnit {}
abstract class Meter implements Unit, _GhostUnit {}
abstract class Foot implements Unit, _GhostUnit {}
extension type Length<U extends Unit>(double value) {
Length<U> operator +(Length<U> other) => Length(value + other.value);
}
Length<U> add<U extends Unit>(Length<U> l1, Length<U> l2) {
return l1 + l2;
}
void main() {
var l1 = Length<Meter>(1.0);
var l2 = Length<Foot>(3.5);
var l3 = Length<Unit>(-1.7178); // Ouch, no compile-time error!
var l4 = l1 + l1; // OK.
var l5 = add(l1, l1); // OK.
var l6 = add(l1, l2); // Yes! Compile-time error.
var l7 = add<Unit>(l1, l2); // Ouch, we can circumvent it!
}
In other words, this is definitely not a complete emulation.
This is a proposal to enable a new relationship in the declaration of a formal type parameter: It should be possible to declare that the corresponding actual type argument must be a proper subtype of the bound, it is not enough to be equal to the bound.
The type parameter
X
is treated the same way asX extends S
, except that it is an error forX
to be equal toS
(that is,X <: S
must be true, as usual, butS <: X
must be false).This mechanism can play a role which is somewhat similar to that of an abstract class: It is able to provide a certain amount of structure, but it cannot be used directly.
The abstract class describes properties of all subtypes, but we can't create an instance of the abstract class itself. The strictly bounded type parameter will constrain the possible actual type arguments as usual, but the type which is used to specify the bound itself cannot be used. You could say that the strict bound turns the bound into an "abstract type argument".
So how could this be useful?
In general, it allows us to denote a set of types that we often cannot enumerate. In the example above
S
is a sealed class, which means that it is possible to enumerate the immediate subtypes ofS
, but with any type which isn't sealed we cannot know that we have included them all. Also, it might be convenient to usestrictly extends
even with a sealed type, because it is more maintainable.A strictly bounded type parameter could be used with bound
Object?
to ensure that a type argument can be any type except a top type, which is something that we can't otherwise express. In particular, this means that the actual type argument cannot bedynamic
, but it can be another nullable type (and it can even beNull
). This is not possible if we just specify the bound to beObject
(which is currently the quasi-standard way to express that a type argument can't bedynamic
).It can also be used with
Object?
in order to ensure that it is a compile-time error to rely on the greatest closure (that is, the default approach taken when type inference has no information).A strict bound could be used with
Object
in order to avoid a subtle type likeFutureOr<Object>
(see https://github.com/dart-lang/sdk/issues/54311 for some background info about why we might want to avoid such types).A strict bound could be used with a sealed type in order to specify a finite, statically known set of possible type arguments to a generic entity, and at the same time preventing the sealed type itself from abstracting over this set of types as a whole (is the "abstract type argument" usage: all the subtypes are "concrete as type arguments" and can be used as actual type arguments, but the sealed type itself is "abstract as a type argument" and cannot be used directly):
Without
strictly
we cannot ensure that the units are respected: