-
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
[inline classes] Support simple union types? #2603
Comments
Besides |
This seems like an attempt to make an untagged union act like a tagged union, plus extra work to detect invalid values. |
This is interesting, but the idea of having It's something I played with before in a package I made a long time ago: union I think being able to do the following is quite important: Union2<int, String> value = 42; Also, would we have some way to normalize the type? Again, with my union package, one issue is that we can end up with There's also the question of "Is |
@lrhn wrote:
I'd say that it is an untagged union. For instance, we could have this: class A {}
class B {}
class C implements A, B {}
void main() {
var u = Union2<A, B>.inject1(C()); // No way to tell whether the `C` is seen as an `A` or a `B`.
} A tagged union would be more like this: class Union2<X extends Object, Y extends Object> {
final X? x;
final Y? y;
Union2.inject1(this.x): y = null;
Union2.inject2(this.y): x = null;
bool get is1 => x != null;
bool get is2 => y != null;
}
void main() {
var u = Union2<A, B>.inject1(C());
print(u.is2); // 'false', definitely.
} The point is that the union which is based on a view type is a cheap approach, and presumably robust enough to be worthwhile. It's just helping us to detect cases where, say, an actual argument in a method call should be an |
@rrousselGit wrote:
Indeed. It just depends on the implications of doing that (possible parsing ambiguities, eating up syntactic space such that other things may then not be possible in the future, etc.etc).
In that case the |
@rrousselGit wrote:
Thanks, that's interesting, I hadn't seen that!
I don't think we can expect Dart to twist and bend a lot in order to make this mechanism optimally convenient. But it might be worthwhile even with those warts.
Union2<int, String> value = 42; That would work right away with var value = Union2<int, String>(42); It's more important in cases like
I don't think so. There's no natural ordering of types. Even an alphabetical ordering would act up if one expression of a type uses import prefixes and the other one doesn't, or it uses different prefixes ( Of course, we could introduce a (small!) set of coercions: view class Union2<X1, X2> {
final Object? it;
implicit Union2.inject1(X1 this.it);
implicit Union2.inject2(X2 this.it);
implicit Union2.reorder(Union2<X2, X1> u): it = u.it;
}
Those are really good questions, and generally my response is that (1) we can achieve a minimal level of support for union types using view classes as illustrated in this issue, but (2) they won't have those nice algebraic properties that you mention.
With an implicit I'd simply recommend that developers use this kind of union types in a simple and straightforward way, and then we may have to admit that normalization of complex union types isn't supported, you just need to avoid getting into that mess. ;-) |
It's tagged-union-like in the sense that it introduces a new type, and doesn't have subtype relations between the union type and the individual values. It's also unsafe in that views will not prevent you from introducing any value into the "union", because the representation type is not a real union type. We can definitely define a type like this, but I'd prefer not to make backends have to recognize it and generate code specially for it. (Like, pretend the two official union types exhaust the possible values, even if it doesn't.) I'd rather introduce a proper "semi-tagged union" construct, like |
@lrhn wrote:
That's definitely a safe bet, in the sense that we can do something without changing the language (other than possibly adding support for I was aiming for a trade-off where we would offer a rather tiny amount of support in the language, e.g., simple well-formedness checks on I think this might be useful, but I admit that it is a risky strategy, because we could end up deciding that this particular kind of union type is far too weak, and we need a real language-level union type with all the nice algebraic properties (normalization, subtyping like Conversely, considering how much work we've done in order to maintain support for nullable types and
), it seems somewhat likely that we could make progress on a minimalistic approach, but aiming for a nice, shiny, complete approach would push the topic way into the future.
The funny thing is that the view class approach does exactly this already. I'd actually expect to have But the view class based approach is much simpler to handle in the language, so we could accept the lack of convenience and expressive power, because it's something that we can actually have soon.
Right, adding an Assigning a However, we would then have a dynamic error (because of the dynamically checked covariance) if we try to add a void main() {
List<Object?> xs = <int>[];
xs.add('Hello!'); // Throws.
} So there's nothing union-type specific about this. In general, view class based union types as type arguments will work as if the type is
That is also how the view class based approach would behave.
Exactly! That's the kind of complications that I wanted to skip by using a plain view class. Similarly, a view class based union type would not have any other members than (1) the ones in |
For reference, using the union package I was able to allow making The trick was to use functions: typedef Union2<A, B> = void Function(
void Function(A value),
void Function(B value),
Object? _c,
);
typedef Union3<A, B, C> = void Function(
void Function(A value),
void Function(B value),
void Function(C value),
);
Union3<A, B, C> union = Union2(A) And I relied on extensions for support assigning extension<T> on Union2<T, T> {
// using extensions, the compiler automatically assign "value" as the closest common type between all generics
T get value {...}
}
Union2<int, double> union;
num value = union.value; // works without a cast So we can get a lot of things done without language support. Although from my experience, it's still painful to use when combining unions. One problem I faced was: As a language feature, maybe we'd be able to use that "reorder"? And do |
Cool! I can't quite see how
The problem that constantly pops up is that there are so many combinations of concrete situations, and it would blow up (in terms of the amount of code as well as the time/space needed to compile code using it) if we enumerate all the situations one by one. For example, there would be k! variants of "reorder" for a union type with k type arguments. So we may not even have a single Another example of those combinatorial explosions is subtyping from k type arguments to k+1 type arguments: We would need to handle each subset of size k from a set of size k+1, that is, k+1 variants, just for that single subtyping relationship (more precisely: to support that kind of assignability, based on So even though we can do a lot, I suspect that it would be more useful to aim at a simple usage for anything like view class based union types. For the sophisticated and highly generic usages we'd want real union types, which all the algebraic laws that we can come up with. But the latter would take longer time to specify and implement than the former.. |
In response to https://github.com/dart-lang/language/issues?q=is%3Aissue+is%3Aopen+union+types+label%3Aunion-types, we could have something like the following in a widely available location (e.g., 'dart:union' or even 'dart:core'):
The modifier
implicit
on constructors was proposed in #309 (for a more specialized purpose, but the idea immediately generalizes to a constructor of any class). The basic idea is that if an expressione
has static typeS
and context typeT
, andS
is not assignable toT
, andT
is a type (parameterized likeC<U1, .. Uk>
, or simplyC
) denoting a class that declares an implicit constructor (say,C.name(S1 s)
) taking exactly one positional parameter and no required named parameters, thene
is transformed into an invocation of that constructor (C.name(e)
orC<...>.name(e)
).It could be used as follows:
The fact that inline classes do not involve allocation of an actual wrapper object ensures that (1) the union type has a zero cost representation at run time (an expression of type
Union2<int, String>
evaluates to anint
or aString
, no extras), and (2) it allows a plain type test (myUnion is int
).The mechanism is not robust (we can easily break it, e.g.,
true as Union2<int, String>
won't throw), but it is only intended to help developers who want to ensure that a given expression has one out of a specified set of types, and we do get the static checking at a reasonable level. For instance, even with an implicitly invoked constructor,f(true)
inmain
would be a compile-time error, becausetrue
doesn't have a type which is assignable toUnion2<int, String>
, and none of the implicit constructors (or any other available code transformation) will make it type correct.The mechanism does not have the algebraic properties that we would expect from a full-fledged union type in the language: We do not have support for
A <: A | B <: B | A <: A | B | C
(no normalization, no reordering), it only has the subtype relationships that we can get from standard covariant type argument based subtyping:Union2<int, String>
is a subtype ofUnion2<num, dynamic>
becauseint <: num
andString <: dynamic
. This is one of the reasons why this mechanism would be a minimalistic version of union types.The support for switch statements where the scrutinee has a union type is perhaps the most tricky part. We could simply omit this and rely on developers writing the switch statements in full. However, it seems quite useful if the compiler/analyzer (or perhaps just the linter) "knows about" union types, such that we can ensure that a sequence of cases testing the type of the scrutinee will actually test exactly the operand types in the union type, and also that it will throw in case there is no match (again, considering
true as Union<int, String>
).[Edit Nov 2: Added reference to the old proposal about implicit constructors, and some more info about the mechanism.]
[Edit, Dec 8: Changed the syntax to the most recent syntax: Views are now inline classes.]
The text was updated successfully, but these errors were encountered: