-
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
Union types #1222
Comments
This comment was originally written by jjo...@google.com Another use-case: In the thread "Too type happy in dart:io?", mythz complained about the verbosity of setting the Content-Type of an HTTP response: response.headers.contentType Instead, it would be nice to say: response.headers.contentType = "application/json; charset=utf-8" However, Anders noted that it's nice to have Content-Type reified as an object, so that you can access properties like so: response.headers.contentType.mimeType In the context described above, type-unions would allow the contentType setter to accept either a ContentType object, or a String; in the latter case, the String would be immediately parsed into a ContentType. |
Any chance of this getting another look from the standards committee? I've run into several cases recently where I really want union types. |
Removed this from the Later milestone. |
Removed Oldschool-Milestone-Later label. |
This comment was originally written by @stevenroose Another important use case is auto-complete in IDE's. If a library declares a method with a dynamic return type, IDE's won't show any autocomplete options. If the method can only return two types of objects, the types could be specified using the union operator and IDE's could show auto-complete values for both types. Example: class MyClass { |
Isn't this relevant for the non-nullable proposal? Ceylon does this really nice thing where |
I find it somewhat strange that this is not higher on the priority list of the dart team. This feature really seems like an essential to me, that should have been in dart 1.0. |
@cgarciae was thinking the same thing myself, union types seem like more bang for the buck. Maybe file a bug against the nullable types DEP? |
According to discussion in dart-lang/sdk#20906 union types were (are?) implemented in the old Dart Editor behind a flag. As far as I understand it, they were being inferred by the editor and used for hints and (possibly) code completion. There was not a syntax to declare a value as having an union type, though, which is useful for documentation purposes and it is what this bug is about. |
I think a great example of a union type use case is JSON:
|
Ah, a recursive union type. No need to make things simple, eh? :) I think that might just be too "wide" a union to actually be useful. To actually use the type, you have to check against almost every type anyway: if (x == null) {
...
} else if (x is bool) {
...
} else if (x is num) {
...
} else if (x is String) {
...
} else if (x is List) { // This should be enough to deduce that x is List<JSON>?
...
} else { // Here you don't have to write (x is Map), that can be inferred.
...
} That does expose an advantage to having the union type. I think we should be smart enough to know when an imprecise type test still only allows one type of the union, so: JSON x = ..;
if (x is List) { .. x is known to be List<JSON> here ... } We can't do that with the current For smaller unions like var result = f(value); // Inferred type (S|Future<S>).
if (result is Future) {
// It's Future<S>
} else {
// It's S.
} Maybe we can even be cleverer: var x = something(); // type (List<int>|Set<int>|Map<int,int>).
if (x is Iterable) {
// x has type (List<int>|Set<int>|(Map<int,int>&Iterable<int>)), not just Iterable<int>.
if (x.contains(42)) print("Huzzah!"); // valid, all three types expose Iterable.contains.
} else {
// x has type Map<int,int> - the List and Set parts could not refute the is test.
if (x.containsKey(42)) print("Yowzer!");
} If we have union types, I want this program to be warning free! :) It might not be as simple as it looks, though :( I'm really doing Boolean algebra on types. A type test creates an intersection of types: T1 x = ...l
if (x is T2) {
// we know x is (T1&T2).
} If The problem is that a positive The possibilities are endless :) |
If we had union types, Flutter APIs that deal with text get much nicer. Instead of this: |
Another case that might be helped by Union Types. Today:
Could be:
|
So is this still being considered? Dart is a great language, but this is one huge thing that it's missing. |
Watch https://youtu.be/9FA3brRCz2Q?t=13m41s and pay attention to FutureOr and whereType. These problems are just begging for union types. Additionally union types are a good way of dealing with null safety. |
I've come from 3 years of TypeScript development over to Dart via Flutter. Many things I like about Dart, some things I don't like so much but can deal with. The lack of union types (and non-nullable types) are really loathsome. For this reason I much prefer using TypeScript. My code is shorter, more type-safe and more self-documenting. If these features make it to Dart I'd feel so much more comfortable with Flutter. |
Well, |
I'm interested in support for algebraic data types, similar to Swift's "enum with associated values" or kotlin's |
An algebraic data type (say, SML style) could be characterized as a tagged union, but union types as discussed here are untagged. So with the algebraic However, since OO objects carry their own (type) tags already, you could claim that we don't want the wrapping, we just want to establish a guarantee that a given type T is sealed (i.e., that T has a finite and statically known set of subtypes, e.g., because they must all be declared in "this file" or something like that). You could then write code that recognizes each of the subtypes of T and does different things with them, similar to pattern matching code with an SML style datatype. I'd think that this would be yet another topic: 'Sealed classes'. ;-) (The reason why I don't think it's just the same topic as algebraic data types in disguise is that sealed classes can be used for other purposes as well, e.g., making sure that we can reason about all implementations of a given method, because we know every one of them statically.) |
Honestly I'd be happy with just the |
That should certainly be a separate issue (and I don't think we have an existing issue which covers that request). |
Created https://github.com/dart-lang/sdk/issues/33079 to cover ADTs |
Union types + classes + typedefEdit: This post has been updated quite a few times to reflect the current discussion status. Also see: Instead of introducing a special syntax just for ADTs I'd like to suggest a solution inspired by TypeScript, Rust and polymorphic variants (example follows below):
So, // Let's define two variants (normal classes) outside of a typedef:
// Syntax sugar from dart-lang/language#1002
class Success<T>(String authToken, T data);
class UnexpectedException(Exception _exception) {
Exception get exception => _exception;
}
// Here we use the same syntax sugar (without "class" keyword)
// as above to define variants inline.
// Every variant that has parentheses is a class defined inline,
// while without parentheses you refer to existing classes.
typedef LoginResponse<T> =
| Success<T> // refers to existing class
| UnexpectedException // refers to existing class
| InvalidCredentials(int code) extends SomeOtherClass() // defines new class
| AccountBlocked(String reason); // defines new class
typedef LoginError = InvalidCredentials | AccountBlocked;
String handleLoginError(LoginError | UnexpectedException error) {
// Simplified switch syntax for exhaustive match
switch (error) {
// Note how we can unpack via the constructor's field definition list
AccountBlocked(reason) {
return reason;
}
UnexpectedException(exception) {
return exception.toString();
}
// With "_" we tell the compiler we're not interested in the "code" attribute
InvalidCredentials(_) {
// ... update UI ...
}
}
// Also a nicer Rust-inspired if-let variant that does pattern matching
if (error is AccountBlocked(reason)) {
// Note that we can directly use the unpacked reason variable
return reason;
}
return "default...";
} Pattern matchingWe should literally match on all variants, no matter if they contain parent and child classes: class Parent();
class Child() extends Parent();
typedef X = Parent | Child | int;
X x = ...;
switch (x) {
Parent() -> ...
Child() -> ... // Would fail at compile-time if this were missing
int -> ...
} TODO: Should we extend Maybe? If you want to match on the class hierarchy instead: switch (x) {
is Parent -> ...
int -> ...
} We could also allow any other binary operator (not just switch (someInt) {
> 10 { ... }
> 0 { ... }
default { ... }
} We could allow String result = switch (x) {
Parent() -> 'parent'
Child() -> 'child'
int -> 'int'
} But how do we define the result when using
Subtyping rulesA union type would behave like the common interfaces implemented by all of its variants. So, The subtyping rules are based on Scala 3:
So, TODO: Intersection types (if also supported)If we also add intersection types (commonly expressed with
TODO: Extensions (once supported by Dart)Extensions would allow adding interfaces to existing variants of union types and maybe they could even add methods to the whole union type (i.e. add an implementation to all variants at once). TODO/Maybe: Interface requirementsWe might also want to have a feature for "require interface X for type T" where T can be a union type: mustImplement LoginResponse<T> with Foo, Bar; Then you'd be statically forced to implement the Type narrowingSimilar to TypeScript, match and if-statements that check the type automatically narrow a variable's type in the matching block. Generic typesIt should be possible to take the union of two generic types ( TODO: What if you How can we apply narrowing to generic types? TODO/Maybe: OverloadingWith overloading you could define functions like this: void doSomething(int x) {}
void doSomething(double x) {}
int | double x = ...;
doSomething(x); If the union type contains both a parent and child class and the overloaded function takes multiple arguments this might require multiple dispatch to work properly. Initially Dart could disallow function overloading with ambiguous union types. TODO/Maybe: Literal typesSomething else I really like about TypeScript is that you can use literals as types (especially strings). This way you can e.g. make APIs safe that only accept certain constants: const KeyA = 'a';
const KeyB = 'b';
// You can even refer to a const here
typedef Key = KeyA | KeyB | 'c' | null;
void setValue(Key key, String value) {
// ...
} With some codegen literal unions could even be used for e.g. making Flutter's Changelog
|
For the improved static checking that union types would give us, it might be useful to take a smaller step in the shape of a lint. See https://github.com/dart-lang/linter/issues/1036 for a request in this direction. |
tl;dr Read the bold text rd;lt @wkornewald, thanks for a nice description of a notion of union types! I do think it's worth pointing out a specific distinction:
These are properties that I'd expect from any proposal about union types, and you should be safe assuming that you'd get them. However, In contrast, union types won't ever allow the compiler to know that any constraints apply to a specific subgraph of the subtype relation, so union types will never serve to improve the opportunities for inlining and other optimizations that may only be valid if you can establish a (modular!) compile-time guarantee that every possible implementation of a given method is known. Similarly, if a human being or proof system is reasoning about the behavior of a certain interface it may be very helpful to know every possible implementation of the methods under scrutiny. Union types are not just weak when it comes to establishing completeness guarantees ("we've seen them all") which are known at the declaration of a type, they're irrelevant. You mentioned one more property, and if I understood it correctly then it's actually just one more example of a difference between a sealed set of classes and a union type:
If you have the sealed set So we shouldn't consider union types and sealedness as alternatives, they are simply different. That said, I think your examples provide a nice illustration of how several independent mechanisms can interact constructively (value classes, class declaration sugar, pattern matching). That's an important motivation when each one of them is considered. |
Lets take an example, |
I agree with you very much, but personally I think that if there is a feature that can have a wider impact, it should have a higher priority, and there is no doubt that union type falls into this category. I've looked at a lot of third-party packages, and about 1/3 of them make me think, "If there was a union type, there would be no need write code like this, or no need to write so much boilerplate code", This is my immature opinion. Maybe one day, AI will help us to better count the needs of developers, perhaps no longer based on some transparent or opaque voting mechanism, but on how people are actually writing code |
+1 |
This feature should be top priority if you ask me. The benefit of implementing this would far outweigh the effort it would take to implement. If you'd implemented Data Classes and Union Types on their own - they would have released much faster (already be in the language by now) & would have improved the language significantly. But you're hogging up all your resources with the gigantic, tedious, beast of burden that is Static Metaprogramming (when all we really asked for was Data Classes) which is still a long time away from release. |
Speak for yourself. |
The static meta-programming looks interesting but it's value is much lower than union types in my opinion. I just don't see any use for it apart from making the language overly complex. Dart was nice and simple. Where was this voted on and why? Union types + native JSON support is the only thing missing in Dart for the teams I work with. (mainly flutter and some GRPC server stuff) I see "dynamic" and the use of (someDynamic is someClass) for APIs & classes that include union types (usually from TypeScript with TRPC) and it sucks in Dart. The union example given in the static meta explanation looks messy 🫣 |
Even using In our projects, when we think "this could use a union type argument" we agree to rethink the entire logic of that component because it usually means something is wrong. I agree that Union Types is a must for Dart to prevail as all the kids on the block want it, however, static metaprogramming is insanely needed for big projects. |
@jodinathan I'm dealing with projects (around 6 at the moment) some with +150k lines of code in Flutter (yes they are terrible). I still don't see a use-case for static meta-programming. Can you give me some examples of why we need it? Unfortunately I see the need for Unions. Just a side note; Typescript you can do "is" as well without guards. Class meta can be attached to the JS for awhile now. |
We have internal server side and frontend side frameworks written in Dart that would benefit a lot from static metaprogramming. We can automate several parts of the project that are currently manually coded.
A builder is too much work to automate that while a simple macro would definitely be an easy help. Another example is that the translation package |
@jodinathan I really don't see this as a higher priority than a core language feature such as Unions though? Hmm. Indeed using a builder can be annoying, we're using annotations with an RPS command to get around our JSON APIs in TRPC. At least it's clear and not hiding anything from the developer. |
Hence why Dart macro will use the new augmentation feature. From what I understood it will be like extending a class in a |
static metaprogramming can be super helpful in so many cases. But I dont think it would be a great coding experience for unions though. I dont think anything can beat the simplicity of this syntax.
|
@dinbtechit exactly. @jodinathan the whole point of extends _$Foo is so that you can easily see it, tap on it, done.. don't hide things. What I see people wanting across teams (Flutter mainly which is basically what Dart is for):
I remember when we didn't have optionals (or non-nullable they call it). Almost sounds like not knowing if something is union ... Sometimes I feel they don't work on real world applications, how meta programing is above types is beyond me. |
I think this is more of a counterexample which speaks against union types, rather then example which speaks in favor of them. Notice that this this type is
In other words you can just use |
@mraleph I think this is a bit off topic. The issue isn't really local issues like this, he's just giving an example, but I think it's valid, I will give an answer below as to why it's obvious. Some APIs force unions on us, and we are currently screwed; using dynamics with 'is' keyword and comments as a solution isn't great. In dart we are missing Utility Types, example But to the reply. I've seen this before though using 'is' inside, example widget parameters.
:-) |
@OllyDixon hence why Dart team is creating the augmentation feature. From what I understand everything will be visible to the developer as it is now but better, for example when generating class source code we are not extending, but implementing it. |
@jodinathan - it is definitely an interesting perspective. I think it's more applicable to the However, the way how I look at unions are... you are just telling the compiler that in a given memory location you can either store TypeA or TypeB (i.e., typedef Padding = double | EdgeInsetsGeometry;
// Usage
Padding myPadding = 8;
// somewhere in the library when using the value
if (myPadding is double) {
// Do something
} else if (myPadding is EdgeInsetsGeometry) {
// Do something else
} else {
// This section will never run because it can either be one of the above types.
} Notice - that we are not merging those 2 types. So when you are using the variable you still have to check the type. The advantage of this is it will enforce the implementer to put in the right datatype at the compile time or else they will get a compile error. I don't know if you need visibility into that because you already know what those types are. For @mraleph earlier point:
That is true. But if you need to self-document you can do something like this. typedef EdgeInsetsAllSides = double;
typedef Padding = EdgeInsetsAllSides | EdgeInsetsGeometry // Now it is verbose yet easy for implementers
// usage
SomeWidget(padding: 8) // Less verbose.
or
SomeWidget(padding: EdgeInsets.only(left: 8)
// This is just an example I know there are other better ways you can do this.
// I am running into more complex scenarios for that I need this feature.
// I am not sure how to explain them here. |
@dinbtechit I think you tagged the wrong user. But about your example:
What I dislike about union types is the feeling of bad performance that this gives me. In your example... Do we really need a runtime condition to something that should be able to be checked at compile time? ie: void geometryPadding(EdgeGeometry foo) {}
void padding(double foo) {} Yeah it is a simple Would be nice to use union types if the compiler can identify and tweak it at compile time: typedef Padding = double | EdgeGeometry;
void padd(Padding myPadding) {
if (myPadding is double) {
print('double! $myPadding');
} else if (myPadding is EdgeGeometry) {
print('geometry! $myPadding');
}
}
dynamic foo = 10;
void main() {
padd(8);
padd(foo);
}
// compile the above into
void main() {
print('double! 8');
print('double! 10');
} Sure the example above is simple, but if we keep all variable definition (that depends on the union types arguments) at the top of the method, the compiler should be able to aways identify and optimize the function calls. |
Why there's still no union type! It's quite surprising to me how this important feature is lacking in dart! |
I don't understand why this got closed. Is there a better dart alternative? What pattern should we follow instead ? |
@searleser97 This issue was closed as a duplicate of #83, which remains open. (This issue was closed with #1222 (comment) which references #83, and the |
Currently Dart does not support function overloads based on parameter type and this makes it verbose/awkward to clone/wrap Javascript APIs which accept multiple types.
For example, WebSocket.send() can accept a String, a Blob or an ArrayBuffer.
In Dart, this can either be exposed as:
WebSocket.sendString, sendBlob, sendArrayBuffer which is verbose
or:
WebSocket.send(Dynamic data), which is no longer a self-documenting API.
It would be great to have some variant of union types, along the lines of JSDoc:
WebSocket.send(String|Blob|ArrayBuffer data);
The text was updated successfully, but these errors were encountered: