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

Modeling a "Result" type is possible, but difficult #3337

Open
matanlurey opened this issue Sep 10, 2023 · 6 comments
Open

Modeling a "Result" type is possible, but difficult #3337

matanlurey opened this issue Sep 10, 2023 · 6 comments
Labels
patterns Issues related to pattern matching. request Requests to resolve a particular developer problem type-inference Type inference, issues or improvements

Comments

@matanlurey
Copy link
Contributor

Playground: https://dartpad.dev/cb41699ff72a41cb9412ef48f47cf824.


I've really enjoyed Dart's 3 class modifiers and pattern matching.

I'm trying to implement a Result<T, E> type, similar to Rust's std::result:

sealed class Result<T, E> {
  const Result();

  const factory Result.value(T value) = ValueResult._;
  const factory Result.error(E error) = ErrorResult._;
}

final class ValueResult<T, E> extends Result<T, E> {
  final T value;

  const ValueResult._(this.value);
}

final class ErrorResult<T, E> extends Result<T, E> {
  final E error;

  const ErrorResult._(this.error);
}

As you can see in my playground, it works! It's just a bit more complicated than I'd expect for a sealed type with only two sub-types (I can see how the implementation as-is works for traditional enum-like types). Here are the individual test cases I wrote:

// Doesn't work, the "else" case doesn't promote willSucceed to ErrorResult, despite the fact that it must be?

if (willSucceed is ValueResult<int, String>) {
  print('value: ${willSucceed.value}');
} else {
  print('error: ${willSucceed.error}');
}
// Similar to above.

if (willSucceed case ValueResult(value: final value)) {
  print('value: $value');
} else {
  print('error: ${willSucceed.error}');
}
// Works, but is the most typing.

switch (willSucceed) {
  case final ValueResult<int, String> result:
    print('value: ${result.value}');
  case final ErrorResult<int, String> result:
    print('error: ${result.error}');
}
// Works, and is probably what I'd use.

switch (willSucceed) {
  case ValueResult(: final value):
    print('value: $value');
  case ErrorResult(: final error):
    print('error: $error');
}

Maybe there is a better way to do this I don't know about or this can serve as inspiration for type inference improvements.

Thanks!

@matanlurey matanlurey added request Requests to resolve a particular developer problem patterns Issues related to pattern matching. type-inference Type inference, issues or improvements labels Sep 10, 2023
@eernstg
Copy link
Member

eernstg commented Sep 10, 2023

I can see that this is mostly about handling values and errors using some kind of case analysis, but if you are also interested in chaining (that is, methods often named as andThen, map, and mapError) then you might find the discussion in dart-lang/sdk#51680 (and issue dart-lang/sdk#51680 (comment)) relevant as well.

@matanlurey
Copy link
Contributor Author

Thanks Eric! I care less about chaining but those are all good references.

@lrhn
Copy link
Member

lrhn commented Sep 11, 2023

It's correct that the "you've exhausted all the other options, so what remains must be the type" promotion isn't something we support for sealed types. It works for union types, FutureOr and nullable types, but that's pretty much it.

One reason is an implementation issue. The "is this switch exhaustive, or what's missing" algorithm is separate from the type inference algorithm, and runs after (because it needs to know the static types to answer that question).
That means that the type inference algorithm, which does the actual promotion, won't have the necessary information. It tries to do a little guessing at whether a switch is exhaustive or not, because it needs that for flow analysis, but then it runs the real exhaustiveness algorithm afterwards to check that it got things right. (That's easier for "must be exhaustive" switches, where inference can assume that such a switch is exhaustive, and then exhaustiveness checks it later and gives an error if it's not)

You'll notice that even switches don't work without writing the type for the second element:

switch (willSucceed) {
  case ValueResult<int, String> _:
    print('value: ${result.value}'); // Works
  case _:
    print('error: ${result.error}'); // Doesn't work, not promoted to `ErrorResult`.
}

(I hope the compiler might be able to use the exhaustiveness check to know that it doesn't need to do a the last check, but that's long after type inference.)

@christopherfujino
Copy link
Member

christopherfujino commented Sep 11, 2023

It works for union types

Dart has union types? oh, I guess you mean FutureOr and nullable types are the two types of supported union types?

@mcmah309
Copy link

mcmah309 commented Dec 9, 2023

@matanlurey If you are interested in Rust types like Result, check out this new library: https://pub.dev/packages/rust_core . Rust's result type has been completely implemented and more.

@Levi-Lesches
Copy link

I filed #3501 to address adding such a type directly to the SDK or a first-party package, and maybe implementing chaining operators such as ??, !, and ?..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patterns Issues related to pattern matching. request Requests to resolve a particular developer problem type-inference Type inference, issues or improvements
Projects
None yet
Development

No branches or pull requests

6 participants