-
Notifications
You must be signed in to change notification settings - Fork 207
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
Non exhaustive switch with generics (Recoverable Errors) #2963
Comments
@rubenferreira97 what version of Dart are you one? The exhaustiveness checking is only very recently implemented. |
@jakemac53 I am experimenting a master branch |
It seems like there may be two separate issues here relating to exhaustiveness checking. void main() {}
void example<T, E>(Result<T, E> result) {
switch (result) {
case Ok():
print('OK');
case Err():
print('Err');
}
}
sealed class Result<T, E> {}
class Ok<T> extends Result<T, Never> {}
class Err<E> extends Result<Never, E> {}
Additionally this example. void main() {}
void example(Container<DivideError> error) {
switch (error) {
case Box<DivideByZero>():
print('divide by zero');
case Box<DivideByNegative>():
print('divide by negative');
}
}
sealed class Container<T> {}
class Box<T> extends Container<T> {}
sealed class DivideError {}
class DivideByZero extends DivideError {}
class DivideByNegative extends DivideError {}
|
The first example in the previous comment looks like either a bug, or a place where the algorithm is not sophisticated enough to correctly enumerate the subtypes. @munificent @johnniwinther thoughts on this one? The second example is legitimately not exhaustive, and the counter-example in the error message is correct. That switch does not match |
Yes, in the first example the hierarchy is considered "non-trivial", though here in the very low end of non-trivial and potential candidate for improvement. |
So it sounds like this is working as intended for now, though possibly something we could improve in the future. While not ideal, I believe you can work around this by making class Ok<T, E> extends Result<T, E> {}
class Err<T, E> extends Result<T, E> {} Hopefully inference could then avoid having to specify both explicitly. Alternatively, you could provide helper construction functions, e.g.: Ok<T, Never> ok<T>() => Ok(); |
Thanks for the quick response! Unfortunately if I make sealed class Result<T, E> {}
class Ok<T, E> extends Result<T, E> {
Ok(this.value);
final T value;
}
class Err<T, E> extends Result<T, E> {
Err(this.error);
final E error;
}
sealed class DivideError {}
class DivideByZero extends DivideError {}
class DivideByNegative extends DivideError {}
Result<int, DivideError> divideNonNegative(int a, int b) {
final error = Err(DivideByZero()); // infers as Err<dynamic, ...>, need to declare as final Err<int, DivideError>
if (b == 0) return error; // error
if (b < 0) return Err(DivideByNegative()); // this works because int is inferred
return Ok(a ~/ b);
} I guess it kinda works, but it seems really fragile. I think I will wait for a better solution. I am aware the team is really busy so it's perfectly normal that the first implementation of exhaustiness will have some imperfections. I am fine with that, but personally (a little bias I know 😉) I think this is a nice feature to aim, express different return paths for error handling. If someone finds a better way to implement this I am all ears. I would like to ask two more things: Could dart allow class declarations inside sealed classes like Kotlin or Rust? Kotlin: sealed class Event<out T: Any> {
class Success<out T: Any>(val value: T): Event<T>()
class Error(val msg: String, val cause: Exception? = null): Event<Nothing>()
} Rust: enum Result<T, E> {
Ok(T),
Err(E),
} Hypothetically, could Dart pull up this magic? I really don't see a way to implement this currently. I can only see it happen at language level. Also, I want to express my sincere appreciation to the Dart team for your hard work on Dart 3.0. Your dedication have resulted in a programming language that has made my work as a developer easier and more enjoyable. |
Yeah, I think you'd really want to make construction be via a helper function as I described above, instead of directly via the constructors unfortunately.
I agree that it would be really nice to handle this. I expect this kind of use case to not be uncommon.
I think @munificent has had some thoughts in that direction, not sure if he's written anything up about it yet. Obviously no promises though, it's certainly not currently on the roadmap.
Thanks for the kinds words! |
@rubenferreira97, you might find dart-lang/sdk#51680 relevant. It isn't about exhaustiveness, but it contains some discussions about the typing of a very similar class hierarchy, and some reasons why you may not wish to use the value |
@eernstg Thanks for pointing me to that issue, it made me realize that there are indeed some problems with this implementation. Ideally I would just want to type Edit: Connecting some dots. @eernstg solution (runtime sealed class _Result<T, E, Invariance extends _Inv<T, E>> {
const _Result();
}
class _Ok<T, E, Invariance extends _Inv<T, E>> implements
_Result<T, E, Invariance> {
final T value;
_Ok(this.value);
}
class _Err<T, E, Invariance extends _Inv<T, E>> implements
_Result<T, E, Invariance> {
final E error;
const _Err(this.error);
}
typedef _Inv<T1, T2> = (T1, T2) Function(T1, T2);
typedef Result<T, E> = _Result<T, E, _Inv<T, E>>;
typedef Ok<T, E> = _Ok<T, E, _Inv<T, E>>;
typedef Err<T, E> = _Err<T, E, _Inv<T, E>>;
sealed class DivideError {}
class DivideByZero extends DivideError {}
class DivideByNegative extends DivideError {}
Result<int, DivideError> divideNonNegative(int a, int b) {
if (b == 0) return Err(DivideByZero());
if (b < 0) return Err(DivideByNegative());
return Ok(a ~/ b);
}
void main() {
final result = divideNonNegative(10, 0);
switch (result) {
case Ok(:final value):
print(value);
case Err(:final error):
switch (error) {
case DivideByZero():
print('Divide by zero');
case DivideByNegative():
print('Divide by negative');
}
}
} But take this weird example: // exhausts
switch(result) {
case Ok():
case Err():
}
// does not exhaust, bug?
switch(result) {
case Ok _:
case Err _:
} |
Yeah, it's weird. Probably correct, but weird. The In In The TL;DR: Always use the object pattern. 😉 |
Sorry if this is a silly question, but why instantiate-to-bounds does not guarantes that the supertype of a type is at least the type itself? Well I understand that the name supertype loses meaning here 😁, I maybe would call it an upper bound. Is there any good reason why the type resolution algorithm behaves different from |
The difference is that The The information is there, but just lkek |
@rubenferreira97, you might want to customize the Dart analysis a bit by putting some directives into analyzer:
language:
strict-raw-types: true
strict-inference: true
strict-casts: true
linter:
rules:
- avoid_dynamic_calls In particular, class A<X> {}
class C<X extends C<X>> {}
void main() {
A a = A<int>(); // 'strict-raw-types': `A` means `A<dynamic>`.
Object o = a;
switch (o) {
case A _: print('A'); // 'strict-raw-types': `A` means `A<dynamic>`.
case C _: print('C'); // No warning.
}
} Note that this mechanism does not report situations where the raw type gets a more complex type argument by instantiation to bound, and that type argument contains I think we could say that there is no need to prefer object patterns over variable patterns in general (they are just different, and they serve different purposes), but variable patterns do have the special pothole that is |
Yeah, unused type parameters make things harder. But keep in mind that when translating a Rust enum to Dart, you don't have to have the same type parameters on every type constructor / subclass. In your example, since sealed class Result {}
class Ok<T> extends Result {
Ok(this.value);
final T value;
}
class Err<E> extends Result {
Err(this.error);
final E error;
} Now each subclass only has the type parameter it cares about. With this, there are no exhaustiveness errors in the following: void main() {
final result = divideNonNegative(10, 0);
switch (result) {
case Ok(:final value):
print(value);
case Err(:DivideByZero error):
print('Divide by zero');
case Err(:DivideByNegative error):
print('Divide by negative');
case Err(:final error):
print('Other error');
}
} Note that the final
I am definitely interested in exploring syntax to make it easier to define a sealed hierarchy of types, but I don't have anything concrete yet. |
@munificent Thank you for the suggestion! Unfortunately, with that example, I think I would lose the main goal I am aiming for, which is type safety at the exhaustiveness level while providing separate paths for "correct" and "error" cases. Assuming we are writing |
Ah, I see. Yes, you lose the ability to pin both the result and error types in the return type type annotation. |
I am trying to implement a custom error handling in dart. I want to be able to represent a correct return (Ok) or an error union (yes, I really hate undocumented exceptions 😁, I would use exceptions only for "panic" situations, unrecoverable errors). Since unions are still not present in Dart I am trying to accomplish this with sealed classes.
I took some error handling examples from different languages and rust error handling (https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html) seems to work well in many cases.
This is my implementation, however the following does not exhaustiveness checks.
First, is there a more "darty" way to represent this? Should this give an error? Since I am using sealed classes I would think that this was Ok.
The text was updated successfully, but these errors were encountered: