xsahil03x / super_enum

Create super-powered dart enums similar to sealed classes in Kotlin
https://pub.dev/packages/super_enum
MIT License
116 stars 13 forks source link

Add more whenX clauses #19

Closed linxydmhg closed 4 years ago

linxydmhg commented 4 years ago

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.

xsahil03x commented 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

astralstriker commented 4 years ago

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?

passsy commented 4 years ago

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'),
    );
  }
astralstriker commented 4 years ago

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);
    }
  }
passsy commented 4 years ago

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);
}
astralstriker commented 4 years ago

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.

xsahil03x commented 4 years ago

Thanks @passsy, Looks good to me too :+1:

xsahil03x commented 4 years ago

@passsy just a last-minute question that should we really generate these methods or we should let the user create them using extension methods?

passsy commented 4 years ago

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");
});
xsahil03x commented 4 years ago

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");
});

@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.

passsy commented 4 years ago

Yes, completely redundant 😉