George
06/17/2022, 7:30 AMpublic interface Payload {
public val data: Any
public val error: Error
}
with factory fun:
public fun <T : Any> Payload(data: T): Payload {
return PayloadImpl(data)
}
public fun errorPayload(error: Error): Payload {
return PayloadImpl(error = error)
}
2️⃣
data class Payload(val data: Any, val error: Error)
George
06/17/2022, 7:39 AMRoukanken
06/17/2022, 8:54 AMResult<T>
, Arrow's Either<E, V>
various other Result<V, E>
in other libraries...George
06/17/2022, 9:06 AMJoffrey
06/17/2022, 3:58 PMit seems more natural option for me to not base all my apis on a final class but rather on an interface/abstractionIt's true to some extent, but I believe it applies more to behaviour than data. I totally would prefer an option 3 with a sealed class, possibly even a generic one. Both examples (with the required nullable fields) will be weird to use in general compared to a sealed class.
George
06/18/2022, 11:26 AMinternal object EmptyData {
override fun toString(): String = ""
}
And was checking for this object
public class PayloadImpl internal constructor(
override val data: Any = EmptyData,
override val error: Error = emptyError()
)
My empty error :
internal fun emptyError(): Error {
return Error("", -1)
}
Even thought at first, it seems counter-intuitive to not represent the "absent state" with nulls, you get a more clean api without nullable data. But you still have to check for something like isErrorEmpty or isDataEmpty which is the corresponding logic for nulls.
Which would u prefer nulls or EmptyObjects ?
Note: i have a requirement for my object creation which requires that either data is empty or error but not both. It is pretty much the same for nulls or empty states. For example i have this:
/**
* Requires that either data is empty or error but not both. Throws
* [IllegalArgumentException] if the contract is violated
*/
protected fun checkState(data: Any, error: Error) {
var flag = true
if (data is EmptyData && error.isEmpty)
flag = false
if (data !is EmptyData && !error.isEmpty)
flag = false
require(flag) { "Either data or error must be empty, but not both. Got data: $data, error: $error" }
}
Roukanken
06/18/2022, 11:31 AMGeorge
06/18/2022, 11:32 AMRoukanken
06/18/2022, 11:35 AMGeorge
06/18/2022, 12:24 PMpublic sealed class SealedPayload
public data class Data<T>(val value: T) : SealedPayload()
public data class SealedError(val message: String, val code: Int) : SealedPayload()
It also plays nice with the generic, as joffrey comment out. Thanks guys for your feedback, appreciate it!Joffrey
06/18/2022, 12:50 PMSealedPayload
be generic itself, otherwise you lose the type info when passing the parent type around, and this makes pattern matching in when
less useful (you can't check for the generic type).
public sealed class Payload<out T> {
public data class Data<out T>(val value: T): Payload<T>()
public data class Err(val message: String, val code: Int): Payload<Nothing>()
}
(Also, note the use of out
to tell the compiler that your class is covariant in T
)
With the generic there, when some function receives Payload<T>
and checks that it's a Data
, it knows which T
it is, and you can write type-safe things like:
fun <T> Payload<T>.valueOrThrow(): T = when(this) {
is Payload.Data -> value
is Payload.Err -> throw SomeException(message, code)
}
George
06/19/2022, 12:54 PMNothing
.
Maybe it's an another question but how come i can write something like this and be ok
public fun test2(): SealedPayload<String> {
return SealedError("interesting", 100)
}
With SealedError being a type of Nothing
But when i make the sealedError type of Any?
i get compiler error type mismatch?.Joffrey
06/19/2022, 1:09 PMT
, which means Payload<Child>
is a subtype of Payload<Parent>
. Because Nothing
is a subtype of everything, Payload<Nothing>
is a subtype of any other Payload
typeGeorge
06/20/2022, 3:12 PMJoffrey
06/20/2022, 3:17 PMNothing
is just a type parameter here. There is no value of this type at runtime anywhere here, or anywhere at all for that matter, because by definition it is a type for expressions that cannot resolve to any value, just to help the compiler in a way.
In which way do you expect it to affect serialization? If a payload is of type Payload.Data
, then this is the class that will be serialized with a value of type T
inside (depending on the serialization library of course). If it's Payload.Error
it will be serialized with the code and message. Do you have anything specific that you think won't work?George
06/20/2022, 3:23 PMSerializer for element of type Nothing has not been found
and i found the corresponding issue on the lib https://github.com/Kotlin/kotlinx.serialization/issues/614. But as you point out this is my desired behavior. Serialize data with value T or serialize Error with code and message.Joffrey
06/20/2022, 3:30 PMNothing
and annotate the type parameterGeorge
06/21/2022, 8:26 AM@file:UseSerializers(NothingSerializer::class)
. Tbh i was just confused how i was supposed to serialize Nothing
but the trick to it is to understand we never serialize Nothing
.Joffrey
06/21/2022, 8:33 AMNothing
is reasonable given that we know it's never going to be serialized