Skip to content
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

Closed
blois opened this issue Sep 5, 2012 · 80 comments
Closed

Union types #1222

blois opened this issue Sep 5, 2012 · 80 comments
Labels
state-duplicate This issue or pull request already exists union-types

Comments

@blois
Copy link

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
Copy link

gbracha commented Nov 6, 2012

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

@DartBot
Copy link

DartBot commented Jul 9, 2013

This comment was originally written by [email protected]


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
Copy link

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
Copy link

kasperl commented Jul 10, 2014

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

@kasperl
Copy link

kasperl commented Aug 4, 2014

Removed Oldschool-Milestone-Later label.

@DartBot
Copy link

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
Copy link

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
Copy link

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
Copy link

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

@jiridanek
Copy link

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.

@srawlins
Copy link
Member

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
Copy link
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 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 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 Support type unions Union types Dec 17, 2016
@sethladd
Copy link

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
Copy link

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
Copy link

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
Copy link

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.

@insidewhy
Copy link

insidewhy 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
Copy link
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
Copy link

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
Copy link
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
Copy link

Zhuinden commented May 8, 2018

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

@eernstg
Copy link
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
Copy link

Created https://github.com/dart-lang/sdk/issues/33079 to cover ADTs

@wkornewald
Copy link

wkornewald commented Jun 9, 2018

Union types + classes + typedef

Edit: 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):

  • | for constructing union types (i.e. an unordered set of types)
    • no special sealed or enum keyword
    • you can combine types (including other unions) from different modules
    • minimal syntax and works ad-hoc in function definitions (great for simple cases: no need to name the union or have a separate definition)
    • if a function expects type A | B | C it's valid to pass a subset type like A | C
    • you can define functions that expect just one variant/case (or any subset) instead of the whole union/enum (catches mistakes at compile-time that ADTs can only catch at runtime)
    • can later be used to express nullability in a generic way
    • you can extend an existing function taking A to A | B without breaking the API
    • a lot more flexible and extensible than ADTs while still allowing exhaustive pattern matching for safety
  • typedef for (optionally) giving a union a name
    • simply reuses the existing Dart syntax, nothing new to learn
  • syntax sugar for defining classes inline (based on Support shorthand class creation syntax #1002 and Add data classes #314)
    • reuses a syntax that could become a standard for class definitions and thus be generally useful
    • reuses the normal class concept (less concepts in the compiler, any optimizations necessary for variants would automatically benefit normal classes, too)
    • allows defining methods or even using extends for variants
    • for complex variant definitions you can move the variant out of the typedef

So, typedef + | + class syntax sugar replaces Kotlin's sealed (or Swift's enum) and is more flexible (every individual concept this is based on is useful on its own):

// 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 matching

We 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 switch (like in the examples above) or introduce a separate match expression?

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 is):

switch (someInt) {
  > 10 { ... }
  > 0 { ... }
  default { ... }
}

We could allow switch/ match to be used as an expression:

String result = switch (x) {
  Parent() -> 'parent'
  Child() -> 'child'
  int -> 'int'
}

But how do we define the result when using {} blocks instead of ->? Should the last statement be treated specially?

switch/ match could also allow matching on the next element from one or more async streams like select on Go's channels (implementing a type-safe async any/or for multiple Futures).

Subtyping rules

A union type would behave like the common interfaces implemented by all of its variants. So, int | double implements num and you can call .round() without matching on the type. You can also pass it to any function taking a num argument.

The subtyping rules are based on Scala 3:

  • A is always a subtype of A | B for all A, B (and A | A gets reduced to just A).
  • If A <: C and B <: C then A | B <: C
  • | is commutative and associative:
    • A | B =:= B | A
    • A | (B | C) =:= (A | B) | C

So, | basically creates a flat unordered set such that (int | int) | int is just int and int | double is the same as double | int. Note that this is slightly different from TypeScript where a union type is an ordered set.

TODO: Intersection types (if also supported)

If we also add intersection types (commonly expressed with &) the subtyping rules get extended with:

  • & is distributive over |:
    • A & (B | C) =:= A & B | A & C

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 requirements

We 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 Foo and Bar interfaces (e.g. directly via inheritance/mixin or via extensions) on each of the types that belong to the union. This feature is not strictly necessary because a missing interface would still raise a compiler error when e.g. trying to access a given method, but it wouldn't raise an error if nobody ever uses a given interface. When writing a reusable package you might want to statically guarantee that the interface you're exposing actually adheres to the spec.

Type narrowing

Similar to TypeScript, match and if-statements that check the type automatically narrow a variable's type in the matching block.

Generic types

It should be possible to take the union of two generic types (S | T).

TODO:

What if you switch/match on the type of S | T? With T = S you'd have two different code paths matching on the the same type. Should the first matching case win or should this result in a compile-time error?

How can we apply narrowing to generic types?

TODO/Maybe: Overloading

With 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 types

Something 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 rootBundle.loadString(path) statically allow only valid paths that actually exist (the typedef of allowed paths would be generated at compile time).

Changelog

  • Using switch instead of match in the examples, but it's still an open question which one should be used
  • Added subtyping rules from Scala 3
  • Explained how matching on Parent | Child would work
  • Using normal classes instead of introducing a new data classes concept. Only syntax sugar is needed.
  • Added short notes about extension methods, overloading, and narrowing.

@eernstg
Copy link
Member

eernstg commented Jun 22, 2018

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.

@eernstg
Copy link
Member

eernstg commented Jul 10, 2018

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:

| for constructing unions [plus properties: can combine types .. from different
modules, A|C <: A|B|C, nullable T is simply T|Null)

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, sealed is a different mechanism with a different set of properties and purposes: It's associated with a specific subgraph S of the type hierarchy, presumably declared in a single library L, and it prevents S from being extended by declaring new subtypes of anything in S anywhere outside L. So if you want to use sealed to emulate union types it's going to be a really weak and restricted emulation.

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:

you can define functions that expect just one variant/case instead of the whole union/enum

If you have the sealed set A, B, C with the relations B <: A and C <: A then we can emulate the unions A|B|C, B, and C, but not A, A|B, A|C, nor B|C. So you don't have to restrict yourself to just the whole sealed set (A|B|C), but you also won't get all subsets. And this just reconfirms that sealedness is not unions.

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.

@Meai1
Copy link

Meai1 commented Apr 28, 2019

Lets take an example, File | Directory.
Often times I dont really know what's so common about the two types I want to be possible as a param, I may not want or view them as common at all. I just happen to want X | Y here. Why should I be required to make a Z? Figuring out what Z could be called is often not trivial, just look at the above.
And even if you do come up with a name that fits both, suddenly the code doesnt look so intuitive and nice at all anymore. (did you manage to come up with inode?)
I mean think about that, I never even had a Z in mind at all. That's a future optimization. In my mind this is a boolean: X or Y. Suddenly the language requires me to step completely out of my thoughts and reason about yet another thing, namely Z that I don't even need yet or maybe never.

@ash0080
Copy link

ash0080 commented Apr 13, 2022

I can't believe this discussion has been going on since 2012, and 10 years later it still hasn't happened. 😱

The team has not infinite resources and has to work with priorities.

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

@duck-dev-go
Copy link

+1

@adifyr
Copy link

adifyr commented Nov 26, 2022

I can't believe this discussion has been going on since 2012, and 10 years later it still hasn't happened. 😱

The team has not infinite resources and has to work with priorities.

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.

@jodinathan
Copy link

beast of burden that is Static Metaprogramming (when all we really asked for was Data Classes)

Speak for yourself.
Static metaprogramming can open a magnitude of development options

@ollyde
Copy link

ollyde commented Nov 26, 2022

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 🫣

@jodinathan
Copy link

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.

Even using dynamic in Dart is a better Union Type usage than in TS.
Dart has smart casting, thus you can obj is SomeClass, in TS you need to use the type guards and other messy stuff.

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.

@ollyde
Copy link

ollyde commented Nov 28, 2022

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

@jodinathan
Copy link

Can you give me some examples of why we need it?

  • Today is too much work to create and configure a builder for a simple source generation.
  • With the augmentation feature we can expand classes without having to use extends _$SomeStuff.

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.
For example, a lot of our enum has a translateName method to be used on frontend:

enum BorderStyle {
  solid(borderSolid),
  dotted(borderDotted),
  dashed(borderDashed);

  const BorderStyle(this.translateName);

  static String borderSolid() => Intl.message('Solid',
      name: 'borderSolid', desc: 'Type "solid" of a border');

  static String borderDotted() => Intl.message('Dotted',
      name: 'borderDotted', desc: 'Type "dotted" of a border');

  static String borderDashed() => Intl.message('Dashed',
      name: 'borderDashed', desc: 'Type "dashed" of a border');

  final String Function() translateName;
}

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 Intl can be entirely rewritten with it.

@ollyde
Copy link

ollyde commented Nov 28, 2022

@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.
Dart is great for that reason, everything was simple and clear (easy to debug) 🚀🕺🏿

@jodinathan
Copy link

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 part file but without having to add the noisy extends _$Foo.

@dinbtechit
Copy link

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.

typedef Padding = double | EdgeInsetsGeometry;

@ollyde
Copy link

ollyde commented Jan 2, 2023

@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.
Extending classes gets REALLY messy, last decade I was a JAVA coder ...

What I see people wanting across teams (Flutter mainly which is basically what Dart is for):

  1. Automatic JSON parsing into objects with readable errors. (Like TypeScript)
  2. Unions, Partial, Omit types (and more, union being number 1)

I remember when we didn't have optionals (or non-nullable they call it).
They were debating if they should have optionals or not, insanity, a langauge without knowing if something is null or not...

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.

@mraleph
Copy link
Member

mraleph commented Jan 2, 2023

I dont think anything can beat the simplicity of this syntax.

typedef Padding = double | EdgeInsetsGeometry;

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

  1. not self-documenting i.e. what does double even mean here? Is it left, right, top, bottom, all of them, left and right, top and bottom, etc? I assume that it means padding from all sides, but it is not self-evident.
  2. there is an intersection between two parts of the union i.e. EdgeInsets.all can already encode padding from all sides.

In other words you can just use EdgeInsetsGeometry and you don't need such union type to begin with. Yep, the caller would need to write EdgeInsets.all(10.0) instead of just writing 10.0. No, that's not a problem - verbosity here leads to clear and readable code.

@ollyde
Copy link

ollyde commented Jan 2, 2023

@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
I'm not saying these are all good, but union definitely is.
I can give examples where each one would be useful though.


But to the reply.

I've seen this before though using 'is' inside, example widget parameters.

SomeWidget(padding: 8) // Less verbose.
or
SomeWidget(padding: EdgeInsets.only(left: 8)) // Also an option if you want it, IDE will suggest :-)

:-)

@jodinathan
Copy link

@jodinathan the whole point of extends _$Foo is so that you can easily see it, tap on it, done.. don't hide things.

@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.
If your generated class isn't used as mixin, you'll have to extend it and end up wasting your primary extension slot.

@dinbtechit
Copy link

dinbtechit commented Jan 3, 2023

@jodinathan - it is definitely an interesting perspective. I think it's more applicable to the num datatype which is basically a combination of int & double. Visibility can be helpful there because you're merging 2 different types.

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., double or EdgeInsetsGeometry).

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:

not self-documenting i.e. what does double even mean here?

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.

@jodinathan
Copy link

@dinbtechit I think you tagged the wrong user.

But about your example:

typedef Padding = double | EdgeGeometry;

// client code
Padding myPadding = 8;

// somewhere in the library when using the value

if (padding is double) {
  // Do something
} else if (padding is EdgeGeometry) {
  // Do something else
}

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

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 if but what if it is used too often by the renderer?

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.
And if that is the case, then I think there would be almost no need to have method overloading

@paramsinghvc
Copy link

Why there's still no union type! It's quite surprising to me how this important feature is lacking in dart!
I don't wanna use the freezed union thing for which I have to generate code. uhh

@searleser97
Copy link

I don't understand why this got closed. Is there a better dart alternative? What pattern should we follow instead ?

@EliahKagan
Copy link

EliahKagan commented Aug 17, 2023

@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 state-duplicate label was applied. See also #1222 (comment) for further context.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
state-duplicate This issue or pull request already exists union-types
Projects
None yet
Development

No branches or pull requests