2

I have an enum with many values; error codes for example, or some official list of coded values. In my application, I have several functions where only a subset of those values is admissible. How can I derive restricted enums that contain only a subset of the original enum?

For example, I have an externally provided dictionary of error codes that model as enum:

enum class ApiError(val: errorCode: Int) {
  INCORRECT_CHARACTER(1),
  MISSING_VALUE(2),
  TOO_SMALL(3),
  TOO_LARGE(4)
}

In one function call, only the TOO_SMALL and TOO_LARGE errors may result, in another only INCORRECT_CHARACTER or MISSING_VALUE. Instead of defining two new enums for these particular error return values, I would like both to somehow reference the complete enum with all error codes.

To be more precise: Assume I have a function fun handleError(error: ApiError); inside this function, I want to be able to write an exhaustive when pattern match that covers all enum cases. However, I also want to be able to pass an argument of a restricted enum type to that same function, where that restricted type can take on only a subset of the enum values, as in the example above.

What comes to mind (but does not work in Kotlin) would be to subclass the ApiError enum while restricting the admissible values in each subclass. Is there a Kotlin solution that does something similar?

The opposite question – to subclass an enum for extension – has been discussed here at length. As far as I understand, the objections there do not apply when restricting the potential enum values.

And just for curiosity: I suppose the above question is some concrete and utterly misspecified version of a some type theoretical problem. Can someone provide pointers to the proper theory and terminology?

Ulrich Schuster
  • 1,670
  • 15
  • 24

1 Answers1

3

What comes to mind (but does not work in Kotlin) would be to subclass the APIError enum while restricting the admissible values in each subclass. Is there a Kotlin solution that does something similar?

Yes, if you need to express a hierarchy, you could use sealed class/interface hierarchies with objects as leaves.

sealed class ApiError(val code: Int) {
    object IncorrectCharacter : ApiError(1)
    object MissingValue : ApiError(2)
}
sealed class SizeError(code: Int): ApiError(code) {
    object TooSmall : SizeError(3)
    object TooLarge : SizeError(4)
}

What you lose here compared to enums is the ability to list all possible values using ApiError.values(). But in this case it might not be an issue.

Also it might not be ideal to serialize (and even more so, deserialize), depending on which serialization library you're using.

Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • Thanks for your reply! From what I understand, you propose to replace the original enum with several sealed classes/ADTs. What I am looking for is some way to keep the complete starting enum (`APIError` in my example), and define derived classes or objects that take their admissible values from the original enum. – Ulrich Schuster Jun 20 '22 at 21:35
  • @UlrichSchuster the whole hierarchy still extends the ApiError base class. It's not an enum anymore but it can work the same: all values are of type ApiError, but some are also of more specific types. Do you have a special constraint about what you can or cannot do to this enum? – Joffrey Jun 21 '22 at 06:59
  • Hm, maybe I still don't understand your proposed hierarchy. Your APIError base class does not include all cases of my original enum, so `SizeError` is not a restriction of the original sum type to fewer values but an extension. What I would like to do is to feed a restricted type to a function that also accepts the original complete/full type. – Ulrich Schuster Jun 21 '22 at 10:57
  • @UlrichSchuster maybe it looks confusing to you because the second sealed class `SizeError` is declared *outside* of `ApiError`, but it is in fact still a subtype of it. If a function receives an `ApiError`, it can by any of the 4 values. If a function receives a `SizeError`, it can only be one of the 2 values. – Joffrey Jun 21 '22 at 13:02
  • Ok, got it. Because the classes are both sealed, Kotlin will take all objects to be variants of the `APIError class`. Thanks! – Ulrich Schuster Jun 21 '22 at 13:56
  • Yes exactly, sorry if I was unclear so far – Joffrey Jun 21 '22 at 14:34