Jakob
01/02/2025, 9:22 AMinline fun <reified T : Base> getThing() =
when (T::class) {
A::class -> "something related to A"
B::class -> "something related to B"
}
Base
is a sealed interface, which looks like
sealed interface Base {
class A : Base
class B : Base
}
Since Base
is sealed, I feel like the compiler should be able to figure out that the when
statement in getThing()
is exhaustive and not require me to add an else
arm, but it's not working like I'm expecting. This example code will not compile unless I add an else
arm. My intention here was that when I eventually add a variant C
to the structure, the compiler would be able to notify me if I forgot to add the corresponding arm to the when
statement in getThing()
but being forced to add an else
there completely defeats this.
I'm relatively new to Kotlin, is there something I'm missing here?Joffrey
01/02/2025, 9:51 AMgetThing<Base>()
?Jakob
01/02/2025, 9:55 AMfun <T : Base> getThing(type: KClass<T>) = when (type) {
A::class -> "something related to A"
B::class -> "something related to B"
}
is nicerJakob
01/02/2025, 9:55 AMJoffrey
01/02/2025, 9:56 AMBase
, so neither A
nor B
Jakob
01/02/2025, 9:56 AMBase
is an interface and not a classJoffrey
01/02/2025, 9:57 AMKClass
Jakob
01/02/2025, 9:57 AMJoffrey
01/02/2025, 9:58 AMJoffrey
01/02/2025, 9:59 AMJakob
01/02/2025, 10:19 AMString
to carry primary keys/IDs for various assets all over the place". This keeps leading to bugs where arguments end up in the wrong place in ways that are not quite obvious (e.g. OrderAccess.getByPaymentId(orderId)
is a piece of code that accidentally snuck in as part of a refactor and lead to confusion when an API endpoint was suddenly returning no data).
Most of the time these bugs are caught by testing or code reviews, but I'm experimenting with a value class AssetId<T : IdType>(val inner: String)
where IdType
is essentially Base
from my example code. My hope is that I can replace e.g. OrderAccess.getByPaymentId(id: String)
with OrderAccess.getByPaymentId(id: AssetId<Payment>)
, allowing the compiler/type system to catch this type of mistake at compile time.
So far so good, but I wanted to build on this to also add some run-time safety. All our asset IDs are prefixed, i.e. a paymentId will always start with OA_PMNT_
. I want to leverage this to add run-time checks to AssetId
by adding a companion getPrefix<T : IdType>()
and checking that the string being wrapped in an AssetId
has the expected prefix in an init block. This is where I'm stumped, since the type information needed is erased at runtime unless the type parameter is reified, and I can't do that on the constructor of a value class blob sweat smileJakob
01/02/2025, 10:21 AMdata class PaymentId(/* ... */) : AssetId
insteadCLOVIS
01/02/2025, 11:30 AMinterface Id<T> {
fun toString(): String
}
value class PaymentId(val inner: String) : Id<Payment> {
init {
require(inner.startsWith("OA_PMNT_")) { "…" }
}
override fun toString() = inner
}
value class SomethingElseId(val inner: String) : Id<SomethingElse> {
// …
}
Reading this, I'm wondering if you're going too far into searching for a general solution. Regular polymorphism with a regular interface and one implementation for each case (especially since they have different requirements) seems more appropriate to me.Joffrey
01/02/2025, 11:33 AMinit
block for checks.
You might not even need the parent Id<T>
interface, if you never need generic functions for accessing a T
from an Id<T>
(for instance your getPaymentById(id: PaymentId)
is not generic and doesn't need the generic type from the ID)Joffrey
01/02/2025, 11:37 AMJakob
01/02/2025, 11:51 AMinit
block for every different ID type (there's a handful of these). Value classes seem to a good solution since they "disappear" at run-time, I'll need to experiment furtherJakob
01/02/2025, 11:51 AMJoffrey
01/02/2025, 11:54 AMrequire
+ startsWith
, extracting a simple function that asserts a prefix should do.phldavies
01/02/2025, 1:59 PMJoffrey
01/02/2025, 2:03 PMphldavies
01/02/2025, 2:03 PMJakob
01/02/2025, 2:03 PMJoffrey
01/02/2025, 2:03 PMJakob
01/02/2025, 2:04 PMphldavies
01/02/2025, 2:05 PMJakob
01/02/2025, 2:07 PMJakob
01/02/2025, 2:10 PMphldavies
01/02/2025, 2:19 PMphldavies
01/02/2025, 2:20 PMKSerializer<T>
impl in the companion objectJakob
01/02/2025, 2:20 PM