language
language copied to clipboard
Unions as parameters
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 {
...
}
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).
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.
Dart already has a way of defining sealed union in OOP style:
Yes, this is well known. It's also much more verbose.
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.
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.
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).
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.
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.
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.
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.
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 intand has 1, -15, etc. as its value case String(): // whenintOrString is Stringand 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 hasintas its value (not 1, -15, etc) case String: // whenintOrString == String, aka hasStringas 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
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.
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"