Closed linxydmhg closed 4 years ago
If that's the case and you don't want to handle all cases, you don't need to use super_enum. Simple enums are good to go for such usage. Sealed classes provide very constrained hierarchies and thus you need to cover all the cases. For more information about Sealed Classes, read here - https://kotlinlang.org/docs/reference/sealed-classes.html
Wouldn't mind adding whenX but you will still need to supply an "OrElse" argument because we want to stick with the exhaustive nature of the when clause. Do you think that it will be better for your use case? Or will aliasing be enough as shown by Matej in his video?
super_enums, unlike normal enums, have fields. Therefore super_enums can't be replaced with normal enums to achieve non-exhaustive when constructs.
Why stick to the exhaustive nature? I also think there should be a non-exhaustive when
construct. Like sealed classes in Kotlin have support for it.
sealed class Result<T> {
class Success<T>(val value: T) : Result<T>()
class Error<T> : Result<T>()
}
var result: Result<String> = Result.Error()
// exhaustive when
when (result) {
is Result.Success -> println("never called")
is Result.Error -> println("called1")
}
// called1
// exhaustive when with default
when (result) {
is Result.Success -> println("never called")
else -> println("else")
}
// else
// non-exhaustive when without default
when (result) {
is Result.Error -> println("called2")
}
// called2
partial
could be a possible name for the non-exhaustive when
function.
/// exhaustive
R when<R>({
@required R Function(Success) success,
@required R Function(Error) error,
}) {
switch (this._type) {
case _Result.Success:
return success(this as Success);
case _Result.Error:
return error(this as Error);
}
}
main() {
result.when(
onSuccess: (data) => print(data.message),
onError: (_) => print('Error Occured'),
);
}
/// non-exhaustive, optional `fallback` case
R partialWhen<R>({
R Function(Success) success,
R Function(Error) error,
R Function(Result) fallback,
}) {
switch (this._type) {
case _Result.Success:
return success?.call(this as Success);
case _Result.Error:
return error?.call(this as Error);
default:
return fallback?.call(this);
}
}
main() {
result.partialWhen(
onError: (_) => print('Error Occured'),
);
}
I suppose that Kotlin's when
is inherently non-exhaustive.
But when when
is used as an expression and the input to it is either an enum value or a sealed class object, it becomes exhaustive. It can also be made into an exhaustive statement by using an extension on it.
Thus, I want something similar to the following:
void whenOrElse({
Function(Success) success,
Function(Error) error,
Function(Result) orElse,
}) {
switch (this._type) {
case _Result.Success:
return success?.call(this as Success);
case _Result.Error:
return error?.call(this as Error);
default:
return orElse?.call(this);
}
}
R partialWhen<R>({
R Function(Success) success,
R Function(Error) error,
@required R Function(Result) orElse,
}) {
switch (this._type) {
case _Result.Success:
return success?.call(this as Success);
case _Result.Error:
return error?.call(this as Error);
default:
return orElse?.call(this);
}
}
I agree, whenX
should return void
if it is non-exhaustive. But in that case it doesn't require orElse
because that would make it exhaustive again.
void whenPartial({
void Function(Success) success,
void Function(Error) error,
}) {
assert(() {
if (success == null && error == null) {
throw "when expression provide at least one branch";
}
return true;
}());
switch (this._type) {
case _Result.Success:
return success?.call(this as Success);
case _Result.Error:
return error?.call(this as Error);
}
}
While it is possible to check for exhaustiveness at runtime - making orElse
optional - it is a bad practice. It doesn't warn via lint about the missing orElse
(using the @required
annotation) when a new case will be added later on.
R whenOrElse<R>({
R Function(Success) success,
R Function(Error) error,
R Function(Result) orElse,
}) {
assert(() {
if (success == null || error == null) {
if (orElse == null) {
final missingBranches = [
if (success == null) "`success`",
if (error == null) "`error`",
];
throw "when expression must be exhaustive add necessary branches $missingBranches or `orElse`";
}
}
return true;
}());
switch (this._type) {
case _Result.Success:
if (success == null) break;
return success(this as Success);
case _Result.Error:
if (error == null) break;
return error(this as Error);
}
return orElse(this);
}
It is better if orElse
is @required
. Note: it should be verified with an assert as well. @required
alone is no guarantee.
R whenOrElse<R>({
R Function(Success) success,
R Function(Error) error,
@required R Function(Result) orElse,
}) {
assert(() {
if (orElse == null) throw "Missing orElse case";
return true;
}());
switch (this._type) {
case _Result.Success:
if (success == null) break;
return success(this as Success);
case _Result.Error:
if (error == null) break;
return error(this as Error);
}
return orElse(this);
}
The void function whenOrElse
is better off with an optional orElse
because it is not needed to cover it. The whole purpose of whenOrElse
is to let the developer choose a subset of the possible types and ignore the rest of the cases.
However when the purpose is to return something, orElse
has to be supplied, just as you pointed out.
Thanks for the tip on the assert
statement.
Will do the required changes as soon as I get free.
Thanks @passsy, Looks good to me too :+1:
@passsy just a last-minute question that should we really generate these methods or we should let the user create them using extension methods?
They have to be generated. Writing them by hand is cumbersome.
I also discovered small problem with void whenPartial
. It doesn't work well with Futures. It's impossible to await
void
. Therefore it should become FutureOr<void> whenPartial
.
enum _SignInResult {
success,
wrongCredentials,
timeout,
}
FutureOr<void> whenPartial<R>({
FutureOr<void> Function(Success success) success,
FutureOr<void> Function() wrongCredentials,
FutureOr<void> Function() timeout,
}) {
assert(() {
if (success == null && wrongCredentials == null && timeout == null) {
throw "provide at least one branch";
}
return true;
}());
switch (_type) {
case _SignInResult.success:
if (success == null) break;
return success(this as Success);
case _SignInResult.wrongCredentials:
if (wrongCredentials == null) break;
return wrongCredentials();
case _SignInResult.timeout:
if (timeout == null) break;
return timeout();
}
}
test("whenPartial awaits future", () async {
final result = SignInResult.timeout();
String value;
final future = result.whenPartial(timeout: () async {
return Future.delayed(const Duration(seconds: 1)).then((_) {
value = "assigned";
});
});
expect(value, isNull);
await future;
expect(value, "assigned");
});
They have to be generated. Writing them by hand is cumbersome.
I also discovered small problem with
void whenPartial
. It doesn't work well with Futures. It's impossible toawait
void
. Therefore it should becomeFutureOr<void> whenPartial
.enum _SignInResult { success, wrongCredentials, timeout, } FutureOr<void> whenPartial<R>({ FutureOr<void> Function(Success success) success, FutureOr<void> Function() wrongCredentials, FutureOr<void> Function() timeout, }) { assert(() { if (success == null && wrongCredentials == null && timeout == null) { throw "provide at least one branch"; } return true; }()); switch (_type) { case _SignInResult.success: if (success == null) break; return success(this as Success); case _SignInResult.wrongCredentials: if (wrongCredentials == null) break; return wrongCredentials(); case _SignInResult.timeout: if (timeout == null) break; return timeout(); } }
test("whenPartial awaits future", () async { final result = SignInResult.timeout(); String value; final future = result.whenPartial(timeout: () async { return Future.delayed(const Duration(seconds: 1)).then((_) { value = "assigned"; }); }); expect(value, isNull); await future; expect(value, "assigned"); });
@passsy Do we need that <R>
in whenPartial
? It looks redundant to me. Also, the assertion won't work if no parameter is provided to the method.
Yes, completely redundant 😉
For example if you have enums like [Success, Error] and you only want to do something if it is an [Error], you have to use when and leave the success empty which can get ugly when there are many enums. I think it would be good to generate functions like whenSuccess, whenError to deal with that, instead of having to import generated files.