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

Strawman union proposal question regarding intersection #2731

Open
bernaferrari opened this issue Dec 19, 2022 · 21 comments
Open

Strawman union proposal question regarding intersection #2731

bernaferrari opened this issue Dec 19, 2022 · 21 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@bernaferrari
Copy link

75% of my union usage in TS is

void method(data: A | B)

As the proposal says, intersection is hard, so maybe for now Dart could avoid data.something where something exists in both A and B.

But... Just having A | B in the parameter and being able to switch between them would be extremely helpful for me!! I hate using dynamic or object. And I hate using a second variable that's not null when the first one is.

So, this would be extremely helpful to me:

int method(data: A | B) =>
switch(data) {
A: return 1, 
B: return 2,
}

Yet I hear "is" is really really expensive. So maybe this is faster than comparing by "is" twice.

Any thoughts?

I'm not sure opening an issue is the correct path, just wanted to contribute to the discussion.

@bernaferrari bernaferrari added the feature Proposed language feature that solves one or more problems label Dec 19, 2022
@ykmnkmi
Copy link

ykmnkmi commented Dec 19, 2022

Dart way to do this:

// Base class for A & B
abstract class AB {
  int get something;
}

class A implements AB {
  @override
  int get something {
    // ...
  }
}

int method(AB data) {
  return data.something;
}

void main() {
  print(method(A()));
}

@bernaferrari
Copy link
Author

Yes, but then you get really limited and you can't do a lot of things. The proposal is a solution for this.

@Wdestroier
Copy link

@ykmnkmi the "Java way" works for simple cases, only when these classes are under our control. The same can't be done for void press(String | int key), dynamic process<T>(Stream<T> | Iterable<T> elements) or String hash(String | List<int> input). The latter because String (and possibly more sealed/final classes in Dart 3) can't be implemented. Another huge issue is naming classes. In the A and B example the super class name is AB. In the hash example the type name could be StringOrListInt, HashInput or possibly another bad name that will polute the namespace. The extra boilerplate may decrease the code quality too. Even if interfaces were the solution, I would still need much more creativity and naming things in programming is very hard sometimes.

@ykmnkmi
Copy link

ykmnkmi commented Dec 19, 2022

@Wdestroier Dart way to do this:

void processStream<T>(Stream<T> stream);
void processIterable<T>(Iterable<T> iterable);

int hash(String input);
int hashBytes(List<int> inputBytes);

For example look at SDK code.

@bernaferrari
Copy link
Author

Thanks @ykmnkmi.

I just want to have colors: List<Color | Gradient>. This alone would be extremely helpful for me.

@bernaferrari
Copy link
Author

This is 80% of what I want, just add a | and a few other things:
image

@munificent
Copy link
Member

With the upcoming support for pattern matching, you will be able to write:

method1(Object e) {
  switch (e) {
    case num n: // Use n...
    case String s: // Use s...
    case bool b: // Use b...
  }
}

@bernaferrari
Copy link
Author

bernaferrari commented Jan 17, 2023

Now if you could just replace Object with num | bool | String.. I would be extreeeeemely happy. Shouldn't be "too much effort" (at least compared to everything else you have planned including most of union features). This alone would help me a lot.

@bernaferrari
Copy link
Author

BTW, does the upcoming pattern matching allow for:

method1(Object e) {
  switch (e) {
    case Area n: // Use n...
    case Circle s: // Use s...
  }
}

?

Because on the proposal I need to use Area(var s), but is there a "wildcard" where 'Area' alone would be valid for anything that contains type Area?

@munificent
Copy link
Member

They are different patterns. A pattern like Area n matches any value of type Area and binds it to a new variable named n. A pattern like Area(:var n) matches a value of type Area, then calls the n getter on that object and binds the result to a variable n.

@bernaferrari
Copy link
Author

bernaferrari commented Jan 18, 2023

What I never understood is where this 'n' came from. Like, if it is var n I'm adding a new variable, right? But what if it is named? Could it be Area(value: var n)? Or is it just syntax sugar to say a new variable is coming as part of the result? lol

BTW, thanks for the explanation, it makes even easier to do what I want. Unionssss, please 😭.

@munificent
Copy link
Member

Like, if it is var n I'm adding a new variable, right? But what if it is named?

With an object pattern, it is always named because we need to know what getter to call. It's just that we allow you infer the name from the variable. Area(:var n) means the exact same thing as Area(n: var n).

Could it be Area(value: var n)?

Yes.

@satvikpendem
Copy link

satvikpendem commented Jan 25, 2023

It's just that we allow you infer the name from the variable. Area(:var n) means the exact same thing as Area(n: var n).

Is this related to #1123, ie the feature in other languages where you can elide the argument name of named arguments if they're the same? In JS for example in an object { n: n } this becomes { n }. Other languages however don't allow you to fully elide the argument name without some sort of marker like a semicolon, to make it less confusing, such that the above example becomes { :n }, or in the case of functions, foo(:n). Does that mean Dart is also getting this feature?

@bernaferrari
Copy link
Author

No, it is just a way of saying "this is the output variable" so you can play with it later. You are kind of declaring something that hasn't appeared yet. It is different, but it is a great idea.

@satvikpendem
Copy link

Ah I see, so it's the bound variable for the matched pattern. Makes sense, it's similar to other languages like OCaml or Rust. Why is var needed however, since we already know that we are binding to that variable? Or I guess, we don't know since there could be another variable named n?

@munificent
Copy link
Member

Other languages however don't allow you to fully elide the argument name without some sort of marker like a semicolon, to make it less confusing, such that the above example becomes { :n }, or in the case of functions, foo(:n). Does that mean Dart is also getting this feature?

We don't currently have any plans to support this, but it's not a bad idea. Right now, the eliding names is only within patterns, not argument lists.

Why is var needed however, since we already know that we are binding to that variable? Or I guess, we don't know since there could be another variable named n?

Correct. There's some more context (though it describes a somewhat outdated version of the proposal) here.

@satvikpendem
Copy link

satvikpendem commented Feb 1, 2023

Thanks, just wanted to clarify another thing. If n is already defined and if it's in a pattern, its value will be pattern matched against. But if it's not defined, it will be a new variable whose value will be set by the binding. Shouldn't the Dart compiler already know whether a variable named n is defined or not, thus obviating the need for var or final even in that case? n could be in a different file but Dart when compiling reads all files so it should find such an n defined elsewhere already.

@bernaferrari
Copy link
Author

With Dart 3 allowing to switch on types, I only miss A | B, so that I stop using objects. I don't need any other union behavior in my apps for now.

@mateusfccp
Copy link
Contributor

With Dart 3 allowing to switch on types, I only miss A | B, so that I stop using objects. I don't need any other union behavior in my apps for now.

You can model this in your type system by using a Either<A, B> object.

@bernaferrari
Copy link
Author

Is there a library that does this? I want to use on Flutter, so it needs to be native.

@mateusfccp
Copy link
Contributor

mateusfccp commented Aug 31, 2023

There are many packages that implements simple Either, but they usually do it only for the sum of two elements, so you may want provide Either2, Either3, Either4 and so on...

A naïve implementation would be:

void myFunction(Either<int, String> value) {
  switch (value) {
    case Left(:final value):
      print('Got an int: $value');
    case Right(:final value):
      print('Got an String: $value');
  }
}

sealed class Either<T, U> {}

final class Left<T, U> implements Either<T, U> {
  const Left(this.value);

  final T value;
}

final class Right<T, U> implements Either<T, U> {
  const Right(this.value);

  final U value;
}

With this you get all the pros of a language-level union type:

  • Static analysis checking you are being exhaustive
  • Your APIs are clear and can't be used wrong (you can't pass an invalid value like when you use an Object)

The only obvious downside is that it is a little more verbose.

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