I am currently trying to grasp the usage of the ResultWrapper class in Kotlin, specifically focusing on its generic implementation. The class is defined as follows:
sealed class ResultWrapper<out T> {
data class Success<out T>(val value: T): ResultWrapper<T>()
data class GenericError(val code: Int? = null, val error: ErrorResponse? = null): ResultWrapper<Nothing>()
object NetworkError: ResultWrapper<Nothing>()
}
By what I understood,T is the response model we are expected to receive in case of Success.In case of Generic or Network Error we dont receive the Success Response Model and hence we used Nothing which is the subclass of all classes and represents a value that never exists.To support ResultWrapper<Nothing>
we made T covariant here as making it covariant will make ResultWrapper<Nothing>
a subtype of ResultWrapper<T>
an hence can be used as return value in safeApiCall.I hope that's correct!
However, I am puzzled about the necessity of making Success a covariant subclass. Since Success always has the success response model T, and not its subtype, why is out T used in the declaration:
data class Success<out T>(val value: T): ResultWrapper<T>()
Additionally, since T is covariant, it can only be used in output positions (i.e., as a return type), but how is it possible for safeApiCall
to use T as a parameter?
suspend fun <T> safeApiCall(dispatcher: CoroutineDispatcher, apiCall: suspend () -> T): ResultWrapper<T> {
return withContext(dispatcher) {
try {
ResultWrapper.Success(apiCall.invoke())
} catch (throwable: Throwable) {
when (throwable) {
is IOException -> ResultWrapper.NetworkError
is HttpException -> {
val code = throwable.code()
val errorResponse = convertErrorBody(throwable)
ResultWrapper.GenericError(code, errorResponse)
}
else -> {
ResultWrapper.GenericError(null, null)
}
}
}
}
}
Your first paragraph is correct.
The superclass needs out
for that Nothing “trick” to work. And nothing in this hierarchy of classes consumes T
so even if we weren’t using <Nothing>
, we would want to increase the flexibility of the class by relaxing the variance.
Variance makes classes less restrictive to use, not more restrictive. It does make defining the class’s own function and property signatures more restrictive, for the benefit of making the class more useful to the code outside it.
It also makes the signature of the class more descriptive, which is an improvement for code readability and understanding.
I’m not sure I understand your last question because your safeApiCall
only has ResponseWrapper<T>
in the return (out) position so it obviously is fine to use it there. Maybe your are confusing the T
of the function with the T
of the ResponseWrapper class, but these are two completely different things. Try renaming it something other than T
in the function and maybe it will help you see what’s going on.
suspend fun <X> safeApiCall(dispatcher: CoroutineDispatcher, apiCall: suspend () -> X): ResultWrapper<X> {
The function here has its own generic type X
. Functions’ generic types are always invariant since there is no need for variance—you don’t cast functions. (Well if we were talking about functional objects, this would get more complicated.) X
is an input to the function and if you called this function with a lambda and without explicitly giving the type, the compiler would implicitly use the output type of the lambda to infer what X
is. Then the function returns a ResponseWrapper
whose generic type is X
. So X
is being passed to ResponseWrapper to be used for its out T
. It is a ResponseWrapper that can produce X’s (at least in the Success case). The covariance of ResponseWrapper isn’t helping us here, but at the call site it could be helping with implicit upcasting, for example.