Union types #4938

Open
blois opened this Issue Sep 5, 2012 · 23 comments

Comments

Projects
None yet
@blois

blois commented Sep 5, 2012

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);

@gbracha

This comment has been minimized.

Show comment Hide comment
@gbracha

gbracha Nov 6, 2012

Contributor

Set owner to @gbracha.
Added this to the Later milestone.
Added Accepted label.

Contributor

gbracha commented Nov 6, 2012

Set owner to @gbracha.
Added this to the Later milestone.
Added Accepted label.

@DartBot

This comment has been minimized.

Show comment Hide comment
@DartBot

DartBot Jul 9, 2013

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
      = new ContentType("application", "json", charset: "utf-8");

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.

DartBot commented Jul 9, 2013

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
      = new ContentType("application", "json", charset: "utf-8");

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.

@justinfagnani

This comment has been minimized.

Show comment Hide comment
@justinfagnani

justinfagnani May 23, 2014

Any chance of this getting another look from the standards committee? I've run into several cases recently where I really want union types.

Any chance of this getting another look from the standards committee? I've run into several cases recently where I really want union types.

@kasperl

This comment has been minimized.

Show comment Hide comment
@kasperl

kasperl Jul 10, 2014

Contributor

Removed this from the Later milestone.
Added Oldschool-Milestone-Later label.

Contributor

kasperl commented Jul 10, 2014

Removed this from the Later milestone.
Added Oldschool-Milestone-Later label.

@kasperl

This comment has been minimized.

Show comment Hide comment
@kasperl

kasperl Aug 4, 2014

Contributor

Removed Oldschool-Milestone-Later label.

Contributor

kasperl commented Aug 4, 2014

Removed Oldschool-Milestone-Later label.

@DartBot

This comment has been minimized.

Show comment Hide comment
@DartBot

DartBot Oct 7, 2014

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 {
  ...
  
  Map|String toJSON([bool stringify = false]) {
    Map jsonMap = _generateMap();
    return stringify ? const JsonEncoder.convert(jsonMap) : jsonMap;
  }
}

DartBot commented Oct 7, 2014

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 {
  ...
  
  Map|String toJSON([bool stringify = false]) {
    Map jsonMap = _generateMap();
    return stringify ? const JsonEncoder.convert(jsonMap) : jsonMap;
  }
}

@cgarciae

This comment has been minimized.

Show comment Hide comment
@cgarciae

cgarciae Jun 23, 2015

Isn't this relevant for the non-nullable proposal? Ceylon does this really nice thing where null es of type Null, so a nullable String is declared as String? which I believe is short hand for the union String | Null.

Isn't this relevant for the non-nullable proposal? Ceylon does this really nice thing where null es of type Null, so a nullable String is declared as String? which I believe is short hand for the union String | Null.

@kasperpeulen

This comment has been minimized.

Show comment Hide comment
@kasperpeulen

kasperpeulen Jun 23, 2015

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.

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.

@seaneagan

This comment has been minimized.

Show comment Hide comment
@seaneagan

seaneagan Jun 23, 2015

@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?

@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?

@jirkadanek

This comment has been minimized.

Show comment Hide comment
@jirkadanek

jirkadanek Oct 19, 2015

According to discussion in #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.

According to discussion in #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.

@srawlins

This comment has been minimized.

Show comment Hide comment
@srawlins

srawlins Sep 9, 2016

Contributor

I think a great example of a union type use case is JSON:

JSON = Null | bool | num | String | List<JSON> | Map<String, JSON>
Contributor

srawlins commented Sep 9, 2016

I think a great example of a union type use case is JSON:

JSON = Null | bool | num | String | List<JSON> | Map<String, JSON>
@lrhn

This comment has been minimized.

Show comment Hide comment
@lrhn

lrhn Sep 9, 2016

Member

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 nun)  {
...
} 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 dynamic.

For smaller unions like A = B | C, a negative B test is as good as a positive C test. Classical example:

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 T2 is a subtype of T1 then the result is just T2. That's what we currently support.
If T1 is a subtype of T2, the test is a tautolgy and you don't get any smarter. the result is still T1.
If T1 and T2 are unrelated, then you just get an intersection type. That may be an empty type, and you can't generally use it for anything.
Now, if T1 is a union type S1|S2, doing intersection should give us (S1|S2)&T2 which should be equivalent to (S1&T1)|(S2&T1).

The problem is that a positive is test can't refute any type because it's generally possible to implement any two interfaces on the same object. The negative test can refute something - if you are a Map, List or Set, then knowing that you are not an Iterable definitely precludes being a List or Set.

The possibilities are endless :)

Member

lrhn commented Sep 9, 2016

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 nun)  {
...
} 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 dynamic.

For smaller unions like A = B | C, a negative B test is as good as a positive C test. Classical example:

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 T2 is a subtype of T1 then the result is just T2. That's what we currently support.
If T1 is a subtype of T2, the test is a tautolgy and you don't get any smarter. the result is still T1.
If T1 and T2 are unrelated, then you just get an intersection type. That may be an empty type, and you can't generally use it for anything.
Now, if T1 is a union type S1|S2, doing intersection should give us (S1|S2)&T2 which should be equivalent to (S1&T1)|(S2&T1).

The problem is that a positive is test can't refute any type because it's generally possible to implement any two interfaces on the same object. The negative test can refute something - if you are a Map, List or Set, then knowing that you are not an Iterable definitely precludes being a List or Set.

The possibilities are endless :)

@munificent munificent changed the title from Support type unions to Union types Dec 17, 2016

@nex3 nex3 referenced this issue in dart-lang/matcher Jan 19, 2017

Open

Could Matcher be changed to be typed as Matcher<T>? #37

0 of 3 tasks complete
@sethladd

This comment has been minimized.

Show comment Hide comment
@sethladd

sethladd May 31, 2017

Member

If we had union types, Flutter APIs that deal with text get much nicer.

Instead of this: title: new Text('Example title') I could do: title: 'Example title' because we could annotate title named parameter has taking either String or Text.

Member

sethladd commented May 31, 2017

If we had union types, Flutter APIs that deal with text get much nicer.

Instead of this: title: new Text('Example title') I could do: title: 'Example title' because we could annotate title named parameter has taking either String or Text.

@sethladd

This comment has been minimized.

Show comment Hide comment
@sethladd

sethladd Jun 2, 2017

Member

Another case that might be helped by Union Types.

Today:

new Padding(
  padding: new EdgeInsets.all(8.0),
  child: const Card(child: const Text('Hello World!')),
)

Could be:

new Padding(
  padding: 8.0,
  child: const Card(child: 'Hello World!'),
)
Member

sethladd commented Jun 2, 2017

Another case that might be helped by Union Types.

Today:

new Padding(
  padding: new EdgeInsets.all(8.0),
  child: const Card(child: const Text('Hello World!')),
)

Could be:

new Padding(
  padding: 8.0,
  child: const Card(child: 'Hello World!'),
)
@watzon

This comment has been minimized.

Show comment Hide comment
@watzon

watzon Dec 21, 2017

So is this still being considered? Dart is a great language, but this is one huge thing that it's missing.

watzon commented Dec 21, 2017

So is this still being considered? Dart is a great language, but this is one huge thing that it's missing.

@mwalcott3

This comment has been minimized.

Show comment Hide comment
@mwalcott3

mwalcott3 Feb 4, 2018

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.

mwalcott3 commented Feb 4, 2018

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.

@ohjames

This comment has been minimized.

Show comment Hide comment
@ohjames

ohjames Mar 18, 2018

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.

ohjames commented Mar 18, 2018

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.

@eernstg

This comment has been minimized.

Show comment Hide comment
@eernstg

eernstg Mar 26, 2018

Member

pay attention to FutureOr and whereType. These problems are just begging for union types

Well, FutureOr has been described many times as a way to check out union types "in a sandbox". However, union types and intersection types in their general forms probably cannot be separated, and there's a huge difference between recursive union types and non-recursive ones, so it's definitely a non-trivial step to leave the sandbox (and it's safe to say that this step is not guaranteed to be taken).

Member

eernstg commented Mar 26, 2018

pay attention to FutureOr and whereType. These problems are just begging for union types

Well, FutureOr has been described many times as a way to check out union types "in a sandbox". However, union types and intersection types in their general forms probably cannot be separated, and there's a huge difference between recursive union types and non-recursive ones, so it's definitely a non-trivial step to leave the sandbox (and it's safe to say that this step is not guaranteed to be taken).

@ZakTaccardi

This comment has been minimized.

Show comment Hide comment
@ZakTaccardi

ZakTaccardi May 4, 2018

I'm interested in support for algebraic data types, similar to Swift's "enum with associated values" or kotlin's sealed class. Is this the right issue for that type of support? Or should I file a separate issue?

I'm interested in support for algebraic data types, similar to Swift's "enum with associated values" or kotlin's sealed class. Is this the right issue for that type of support? Or should I file a separate issue?

@eernstg

This comment has been minimized.

Show comment Hide comment
@eernstg

eernstg May 4, 2018

Member

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 datatype Data = Int of int | Char of char; you'd need to unwrap a given Data to get to the int resp. char that it contains, but with a union type you'd just have an int or a char at hand already, with no wrapping. This matters in a lot of ways, so I'd consider algebraic data types to be a different topic.

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.)

Member

eernstg commented May 4, 2018

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 datatype Data = Int of int | Char of char; you'd need to unwrap a given Data to get to the int resp. char that it contains, but with a union type you'd just have an int or a char at hand already, with no wrapping. This matters in a lot of ways, so I'd consider algebraic data types to be a different topic.

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.)

@Zhuinden

This comment has been minimized.

Show comment Hide comment
@Zhuinden

Zhuinden May 8, 2018

Honestly I'd be happy with just the when keyword from Kotlin to simplify if elseif elseif else

Zhuinden commented May 8, 2018

Honestly I'd be happy with just the when keyword from Kotlin to simplify if elseif elseif else

@eernstg

This comment has been minimized.

Show comment Hide comment
@eernstg

eernstg May 8, 2018

Member

That should certainly be a separate issue (and I don't think we have an existing issue which covers that request).

Member

eernstg commented May 8, 2018

That should certainly be a separate issue (and I don't think we have an existing issue which covers that request).

@ZakTaccardi

This comment has been minimized.

Show comment Hide comment
@ZakTaccardi

ZakTaccardi May 8, 2018

Created #33079 to cover ADTs

Created #33079 to cover ADTs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment