Skip to content

Unions as parameters #3608

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

Open
mnordine opened this issue Feb 5, 2024 · 13 comments
Open

Unions as parameters #3608

mnordine opened this issue Feb 5, 2024 · 13 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@mnordine
Copy link
Contributor

mnordine commented Feb 5, 2024

I read https://github.com/dart-lang/language/blob/main/working/union-types/nominative-union-types.md, and see the proposed syntax is something like:

typedef UrlThingy = Url | String;
Future<Response> get(UrlThingy url) async {
  ...
}

Could we just do:

Future<Response> get(Url | String url) async {
  ...
}
@mnordine mnordine added the feature Proposed language feature that solves one or more problems label Feb 5, 2024
@lrhn
Copy link
Member

lrhn commented Feb 6, 2024

Definitely possible. The linked proposal tries to deliberately avoid some of the complexities of structural union types, which is why it choses to not allow you to write a union type without giving it a name.

What you have here is probably either structural union types, but only in parameter position, or general union types.
Those are also possible designs, with some much harder design issues and maybe requring different tradeoffs.
See fx: #83 #1222 #2285 #2711 (some closed as duplicates of #83, but still contain some discussions).

@AlexanderFarkas
Copy link

AlexanderFarkas commented Feb 12, 2024

Dart already has a way of defining sealed union in OOP style:

sealed class UrlThingy {}
class UrlCase extends UrlThingy {
  final Url url;
  UrlCase(this.url);
}
class StrCase extends UrlThingy {
  final String string;
  StrCase(this.string);
}

P.S. Though I find it very convenient to use the same patterns I use in typescript, it's often useful to understand why one language has some feature, while other doesn't. Dart is mostly OOP language with some QoL features from functional paradigm. So in Dart you write (mostly) in OOP style.

If you need to define common operation on UrlThingy in your code, you would have to write a function which accepts the same union and then branch out implementations (which is purely functional style).

In my solution above, it would be defined on UrlThingy as abstract method (which IMO is the right place) and then implemented in every case.

@mnordine
Copy link
Contributor Author

mnordine commented Feb 12, 2024

Dart already has a way of defining sealed union in OOP style:

Yes, this is well known. It's also much more verbose.

@munificent
Copy link
Member

Dart already has a way of defining sealed union in OOP style

Sealed types in Dart model sum types, not union types. This article does a good job of explaining the difference.

@bernaferrari
Copy link

With LLM everywhere, it is 10x easier to develop in TS where I can say to Gemini await GoogleAI(prompt: "Answer this", schema: { answer: "one" | "two" | "three" } and having the whole LLM output type-safe, using the same type I originally passed, so I can do result.answer and it just magically works.

This is someone proposing unions on Kotlin with Java compiliation, doesn't seem like it would be hard for Dart either. Right now it is almost possible to do unions in Dart, just miss the "|" syntax, but I can use Object and switch everywhere. It is just ugly.

image

@skylon07
Copy link

skylon07 commented Jun 6, 2024

I can say to Gemini await GoogleAI(prompt: "Answer this", schema: { answer: "one" | "two" | "three" }

This isn't quite the same thing as union types... This seems more like defining a new enum type rather than a "union" type. That is, it's the difference between 1 | 2 | "one" | "two" (listing values) and int | String (listing types).

@bernaferrari
Copy link

you can do anything... "one" | "two" | int, anything is allowed. I just gave a practical example, where making your own enum for this single usage is completely unpractical and hard, because once you have 40 values you need 40 enums.

@skylon07
Copy link

skylon07 commented Jun 6, 2024

you can do anything ... anything is allowed.

Sorry, let me clarify. I wasn't trying to say what you were suggesting shouldn't be allowed, I was just explaining that the concept of a "value union" ("one" | "two" | "three"), aka an enumeration, is different than a "type union" (int | String), in the same way declaring a value (var i = 5 or var i = int) is different from declaring a type (int i). This issue seems to be specifically about "type unions", although that was just my interpretation.

To your point however, the shared syntax is interesting. I'd be curious to explore how | could be used in dart to shortcut enumeration types. It would be cool if you could do (int | {"one" | "two" | "three"}) myValue or something like that.

once you have 40 values you need 40 enums.

Not sure I understand what you mean here... Do you mean you would need a new name for each type of enum you create? Doing a "value union" like {"one" | "two" | "three"} would make such a type anonymous, which I think is what you're trying to argue for. If so, I agree, that'd be a cool feature. It's just not the one being suggested here.


Edit: Thinking about this more, I would actually suggest clarifying in the docs for this feature that this is not a replacement for enums. I bet a lot of people coming primarily from JS/TS backgrounds or languages with similar features could potentially be confused by what this feature is meant to do.

@bernaferrari
Copy link

I think "one" | "two" is just a consequence and should be allowed as well. Just like const errorCode: 404 | 403 | 402 | 401 is useful as well, and with the switch I wouldn't need to provide a default because it would be exhaustive.

@skylon07
Copy link

skylon07 commented Jun 7, 2024

I think "one" | "two" is just a consequence and should be allowed as well.

For this issue's feature request (that is, "type unions"), I don't think "one" | "two" would be a (simple) consequence of adding the feature. As I said before, what you're suggesting is more like "value unions" or anonymous enums rather than "type unions". The distinction is important because of the possible syntax clash, and would require a different spec/implementation in the guts of dart to get it working than the proposal referenced by this issue. What I mean by "the distinction" is that int | String could mean two different things, and is the difference between this code

// an example of "type unions", what this issue suggests
void myFunction(int | String intOrString) {
  print(intOrString); // prints 1 or -15 or "some string"
  print(intOrString.runtimeType); // either prints "int" or "String"

  switch (intOrString) {
    case int():
      // when `intOrString is int` and has 1, -15, etc. as its value
    case String():
      // when `intOrString is String` and has "some string", etc. as its value
  }
}

and this code

// an example of "value unions", what you're suggesting
void myFunction(int | String intOrString) {
  print(intOrString); // prints "int" or "String"
  print(intOrString.runtimeType); // always prints "Type"
  
  switch (intOrString) {
    case int:
      // when `intOrString == int`, aka has `int` as its value (not 1, -15, etc)
    case String:
      // when `intOrString == String`, aka has `String` as its value (not "some string", etc)
  }
}

Where you've made an argument for the benefits of what you're talking about, I'd suggest making a new, separate issue where you can specify your idea and the use cases you came up with. I think your idea deserves it.

@bernaferrari
Copy link

bernaferrari commented Jun 7, 2024 via email

@lrhn
Copy link
Member

lrhn commented Jun 8, 2024

@bernaferrari

When the "only thing missing" is a completely new kind of type added to the type system, that still deserves its own issue. It's not a union type in the normal meaning of that's phrase, because it's not a union of types.

If every value was its own "type", then this kind of types would be a consequence of union types.
Today values are not types, and what you're asking for is not union types, nor a trivial consequence of union types.

@ghost
Copy link

ghost commented Jun 8, 2024

The "union of values" can be modelled as an enum:

abstract class RawValue<T> {
  T get rawValue;
}
enum Answer implements RawValue<String> {
  one("one"),
  two("two"),
  three("three");
  final String rawValue;
  const Answer(this.rawValue);
}

This is roughly how Swift implements the feature, though it provides an extra layer of (magic) sugar:

enum CompassPoint: String { // extending String triggers magic emergence of "rawValues"
    case north, south, east, west
}
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

6 participants