language icon indicating copy to clipboard operation
language copied to clipboard

Unions as parameters

Open mnordine opened this issue 1 year ago • 13 comments

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 avatar Feb 05 '24 20:02 mnordine

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

lrhn avatar Feb 06 '24 17:02 lrhn

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.

AlexanderFarkas avatar Feb 12 '24 11:02 AlexanderFarkas

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

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

mnordine avatar Feb 12 '24 13:02 mnordine

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.

munificent avatar Mar 11 '24 22:03 munificent

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

bernaferrari avatar Jun 03 '24 18:06 bernaferrari

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

skylon07 avatar Jun 06 '24 19:06 skylon07

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.

bernaferrari avatar Jun 06 '24 21:06 bernaferrari

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.

skylon07 avatar Jun 06 '24 22:06 skylon07

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.

bernaferrari avatar Jun 07 '24 01:06 bernaferrari

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.

skylon07 avatar Jun 07 '24 22:06 skylon07

But switch already has full support into distinguishing it, the only thing missing is a type to make this possible.

On Fri, Jun 7, 2024, 19:35 Taylor Brown @.***> wrote:

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 https://github.com/dart-lang/language/blob/main/working/union-types/nominative-union-types.md. 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 suggestsvoid 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 suggestingvoid 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.

— Reply to this email directly, view it on GitHub https://github.com/dart-lang/language/issues/3608#issuecomment-2155656867, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACVXFISI6YDY6HQGKWCUB3ZGIYRZAVCNFSM6AAAAABC2YCI66VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNJVGY2TMOBWG4 . You are receiving this because you commented.Message ID: @.***>

bernaferrari avatar Jun 07 '24 22:06 bernaferrari

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

lrhn avatar Jun 08 '24 16:06 lrhn

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"

tatumizer avatar Jun 08 '24 16:06 tatumizer