Conditional Nullable Types

Consider the following example

data class DeviceResponse<T>(val status: ConnectionStatus,val data:T?,val errorMessage:String?)

ConnectionStatus can be SUCCESS, UNKNOWN_FAILURE, ATTACHMENT_NOT_MOUNTED,…etc

in case of success the data is non-null and errorMessage is null
in case of failure errorMessage is non-null and data is null
[values - data, errorMessage are depending on the connectionStatus value]
is there any way I can tell this to the compiler and compiler can do a smartcast and also impose a restriction.
or any better approach of doing this?
Thanks in advance.

Seems a good fit for a sealed class with 2 implementations, one for success and one for failure.

5 Likes
sealed class DeviceResponse<T>() {
    companion object {
        fun <T> success(data: T): SuccessDeviceResponse<T> {
            return SuccessDeviceResponse(data)
        }

        fun failure(reason: OperationFailureReason): FailDeviceResponse {
            return FailDeviceResponse(reason)
        }
    }
}

data class SuccessDeviceResponse<T>(val data: T) : DeviceResponse<T>()

data class FailDeviceResponse(val reason: OperationFailureReason) :
    DeviceResponse<Nothing>()

Currently using sealed classes.
Wanted to know if it is possible to tell the compiler in someway to achieve this only using data classes. Thanks.

You could use contracts to enable smart casting:

val result = getResultContainer() // made up class for example
result.assertSuccess() // smart casts the type on rerurn
// Or maybe
if (result.isSuccess()) { ... } // Smart casts type on true (and false)

On mobile so I can’t write out a runnable example atm. The main point is contracts can allow for telling the compiler which subtype you have and information about nullability.

However, normal checks work fine in most cases:

if (result is Success) { ...}
2 Likes

New to contracts. So I have to create function isSuccess() that tells the compiler about nullability of data and errorMessage fields.
but I cannot reference field names in contract right? (I’ve read this from a blog post.)
So how do I apply the contract.
contract{
return(true) implies result.data!=null && result.status = SUCCESS
}

Thanks.

Maybe I’m missing something but what you want is not really possible with contracts right now. I don’t think contracts will let you get out of using a sealed class.
The best you can do is add an extension method for DeviceResponse that works just like a type check, so instead of writing result is Success you could call result.isSuccess() and have it smartcast.

However result.assertSuccess() is a nice idea. I think it would look something like this

fun DeviceResponse.assertSuccess() {
   contract {
        return implies this is SuccessDeviceResponse
   }
   if (this !is SuccessDeviceResponse) error()
}

However if the restrictions on contracts haven’t changed (I’m still on 1.4) it has to be an extension function and you would still need the sealed class structure.

2 Likes

Yeah, contracts are probably not the best solution in this case. My intention was to make sure someone mentioned it :+1:

Contracts might be valuable is if you want to do more complex work that a simple instance check can’t do. For example; if you had multiple parameters that influenced the result. Or if you allowed “unsuccessful” instances of a Success class (and some fancy visibility restrictions), or any case where instance checks don’t tell the whole story and the cast is only one of multiple effects of the function.

1 Like