I have a set of data classes that share some common fields, So ideally I'd like to declare those in a supertype (Message in this example), and be able to write functions that operate on the supertype if they need access to these common fields (messageId in this example).
fun operate(m: Message) {
use(m.messageId)
}
I tried to accomplish this by extending my data classes from a sealed class.
Data classes may extend sealed classes, but not I'm not sure how/if they can accept arguments required by the "supertype" sealed class.
Extending a regular class from a sealed class compiles just fine.
sealed class Message(val messageId: String)
class Track(val event: String, messageId: String): Message(messageId)
However, changing it to a data class doesn't compile ("Data class primary constructor must have only property (val/var) parameters.").
sealed class Message(val messageId: String)
data class Track(val event: String, messageId: String): Message(messageId)
Declaring the parameter as a property also doesn't compile ("'messageId' hides member of supertype 'Message' and needs 'override' modifier'").
sealed class Message(val messageId: String)
data class Track(val event: String, val messageId: String): Message(messageId)
Opening the supertype property and overriding it in each of the base classes compiles fine:
sealed class Message(open val messageId: String)
data class Track(val event: String, override val messageId: String): Message(messageId)
Ideally I would like something close to Option 2 - it allows me to combine the best of both worlds.
Otherwise, it seems my options are either handrolling my own data class functionality (copy, hashcode, equals etc) with option 1, or live with a compromise by opening up up the supertype properties with option 4.
Options 3 and 4 would result in the class holding messageId twice. Once in the new class and once in its superclass.
The solution is to declare but not define the variable in the superclass:
sealed class Message {
abstract val messageId: String
}
data class Track(val event: String, override val messageId: String): Message()
This will make the messageId available on Message, but delegates the storage to whatever implements it.
Related
I am trying to use Kotlin serialization (Kotlin 1.7.2, kotlinx.serialization 1.4.1) for value classes that implement a sealed interface:
#Serializable
sealed interface Power {
val value: Int
}
#Serializable
#JvmInline
value class PowerWatt(override val value: Int) : Power
#Serializable
#JvmInline
value class PowerHp(override val value: Int) : Power
When attempting serialization to Json, like so:
#Test
fun `Correctly serialize and deserialize a value class that implements a sealed interface`() {
val power: Power = PowerWatt(123)
val powerSerialized = Json.encodeToString(power)
val powerDeserialized = Json.decodeFromString<Power>(powerSerialized)
assertEquals(power, powerDeserialized)
}
I run into the following error:
kotlinx.serialization.json.internal.JsonDecodingException: Expected class kotlinx.serialization.json.JsonObject as the serialized body of Power, but had class kotlinx.serialization.json.JsonLiteral
at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:94)
at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:81)
at kotlinx.serialization.json.Json.decodeFromString(Json.kt:95)
How to make this work? Am I missing something?
The answer was provided in a Kotlin Serialization GitHub Issue here. For value classes, the wrapped underlying type is serialized directly. Hence, there is not wrapping JSON object where the type field for the polymorphic serializer could be inserted.
I'm building an ORM for use with jasync-sql in Kotlin and there's a fundamental problem that I can't solve. I think it boils down to:
How can one instantiate an instance of a class of type T, given a
non-reified type parameter T?
The well known Spring Data project manages this and you can see it in their CrudRepository<T, ID> interface that is parameterised with a type parameter T and exposes methods that return instances of type T. I've had a look through the source without much success but somewhere it must be able to instantiate a class of type T at runtime, despite the fact that T is being erased.
When I look at my own AbstractRepository<T> abstract class, I can't work out how to get a reference to the constructor of T as it requires accessing T::class.constructors which understandably fails unless T is a reified type. Given that one can only used reified types in the parameters of inline functions, I'm a bit lost as to how this can work?
On the JVM, runtime types of objects are erased, but generic types on classes aren't. So if you're working with concrete specializations, you can use reflection to retrieve the type parameter:
import java.lang.reflect.*
abstract class AbstractRepository<T>
#Suppress("UNCHECKED_CAST")
fun <T> Class<out AbstractRepository<T>>.repositoryType(): Class<T> =
generateSequence<Type>(this) {
(it as? Class<*> ?: (it as? ParameterizedType)?.rawType as? Class<*>)
?.genericSuperclass
}
.filterIsInstance<ParameterizedType>()
.first { it.rawType == AbstractRepository::class.java }
.actualTypeArguments
.single() as Class<T>
class IntRepository : AbstractRepository<Int>()
class StringRepository : AbstractRepository<String>()
interface Foo
class FooRepository : AbstractRepository<Foo>()
class Bar
class BarRepository : AbstractRepository<Bar>()
fun main() {
println(IntRepository::class.java.repositoryType())
println(StringRepository::class.java.repositoryType())
println(FooRepository::class.java.repositoryType())
println(BarRepository::class.java.repositoryType())
}
class java.lang.Integer
class java.lang.String
interface Foo
class Bar
In your own CrudRepository you can add a companion object with an inline fun which is responsible to instantiate your repository by passing to it the corresponding class.
class MyCrudRepository<T> protected constructor(
private val type: Class<T>,
) {
companion object {
inline fun <reified T : Any> of() = MyCrudRepository(T::class.java)
}
fun createTypeInstance() = type::class.createInstance()
}
I have a simple inheritance tree in my Kotlin project, where a base class is extended by a data class. I cannot declare construction of my data class without overriding the parameters from the base class
I've noticed that this would work if I wasn't extending in a data class:
open class Base(
val first: String,
val second: String
)
class Child(
first: String,
second: String,
val third: List<String>
) : Base(first, second)
This is what I ended up with currently:
open class Base(
open val first: String,
open val second: String
)
data class Child(
override val first: String,
override val second: String,
val third: List<String>
) : Base(first, second)
But I would like to be able not to override the constructor parameters, because I'm not really overriding them. I just need to take them in my Child constructor to be able to pass them to Base.
Having a base class like this and a derived data class, you have to override its properties or separate them, because all primary constructor parameters of a data class must also be declared as properties:
— All primary constructor parameters need to be marked as val or var;
However, based on what your goal really is, you can transform your code in one of the following ways:
Declare the properties in Child as separate, unrelated properties:
open class Base(
open val first: String,
open val second: String
)
data class Child(
val childFirst: String,
val childSecond: String,
val third: List<String>
) : Base(childFirst, childSecond)
This will allow you to have separate implementations for the properties if you need it, storing the values passed as childFirst and childSecond in the Child and probably altering them in some way in the implementation of Base.
Make Base an interface:
interface Base {
val first: String,
val second: String
}
data class Child(
override val first: String,
override val second: String,
val third: List<String>
) : Base
This ensures that Base doesn't have an implementation that stores property values in addition to the Child's properties with backing fields (those will consume additional memory, but, as the propeties are overridden, Base will consistently see the values of the Child's backing fields as first and second).
Make Base an abstract class with abstract properties:
abstract class Base {
abstract val first: String,
abstract val second: String
}
data class Child(
override val first: String,
override val second: String,
val third: List<String>
) : Base()
This follows a similar purpose: Base won't store the property values in its implementation needlessly duplicating the properties of Child.
Make Child a normal class, manually implementing those of the functions that are generated for data classes which you actually need.
I (often) have a resource with two states, pre-created and post-created, where both states have the same fields except for an id field. id is null in the pre-created state and non-null in the post-created state.
I would like to define and use this resource in a clean and type-safe way.
It's common to represent this ID field as a nullable, which handles both scenarios with minimal boilerplate in the class definition. The problem is that it creates a lot of boilerplate in the business logic because you can't assert whether a resource is pre-created or post-created by looking at its type.
Here is an example of the nullable approach:
data class Resource(val id: String?, val property: String)
This is simple to define, but not as simple to handle with due to lack of compile-time guarantees.
Here's an example of a more type-safe approach:
sealed class Resource(val property: String) {
class WithoutID(property: String): Resource(property)
class WithID(val id: String, property: String): Resource(property)
}
This allows me to pass around Resource.WithID and Resource.WithoutID, which have all the same fields and methods, except for id.
One inconvenience with this type-safe approach is that the resource definition code gets quite bloated when you have many property fields. This bloating makes the code harder to read.
I'm wondering if there's an alternative approach with less boilerplate, or if Kotlin has any features that make this kind of thing simpler.
What about defining
sealed class MayHaveId<T> { abstract val record: T }
class WithId<T>(val id: String, override val record: T): MayHaveId<T>()
class WithoutId<T>(override val record: T): MayHaveId<T>()
class Resource(val property: String)
// and other similar types
and using WithId<Resource> and WithoutId<Resource>? In Scala you could add an implicit conversion from MayHaveId<T> to T, but not in Kotlin, alas, nor can you write : T by record. Still should be clean enough to use.
One of the options is to get into composition relying on properties inside interfaces.
interface Resource {
val property: String
}
interface WithId : Resource {
val id: Int
}
interface WithOtherField : Resource {
val otherField: Any
}
class WithoutIdImpl(override val property: String) : Resource
class WithIdImpl(override val id: Int, override val property: String) : WithId
class WithIdAndOtherField(
override val id: Int,
override val otherField: Any,
override val property: String) : WithId, WithOtherField
I didn't get from your example, how you're going to switch between two states of Resource. So probably there is a gap to overcome.
Probably, Smart casts will allow to switch states.
Here is the problem I am trying to resolve, I am trying to use a void type as a generic type:
class Parent {
private abstract class Item<out T>(val data: T)
// This subclass should contain data
private class ItemContent(val data: String): Item<String>(data)
// This subclass doesn't contain data
private class ItemNoContent: Item<Any?>(null)
}
Some base classes like ItemNoContent doesn't contain meaningful data so I make ItemNoContent extends Item(null). It works but I feel that the use of Any? and null is inappropriate here. Is there a more Kotlin way to solve this optional generic problem?
You can also use Item<Unit>(Unit) which represents a void value in Kotlin.
Some base classes like ItemNoContent doesn't contain meaningful data
Then why extend a class which is supposed to have it? While Unit and null are both options, consider also
private abstract class Item<out T>
private abstract class ItemWithContent<out T>(val data: T) : Item<T>
...
// object may make more sense than class here
private object ItemNoContent : Item<Nothing>()
I would tweak the inheritance like this:
abstract class Item
abstract class ItemWithContent<T>(val d: T): Item()
class ItemWithStringContent(d: String): ItemWithContent<String>(d)
class ItemWithNoContent: Item()
This way, there is not need to use Unit or Nothing.
Usage:
fun main(args: Array<String>){
val t: Item = ItemWithStringContent("test")
println((t as? ItemWithStringContent)?.d)
}