Ryan Smith
04/13/2023, 11:32 PMRyan Smith
04/13/2023, 11:32 PMsealed interface Example {
val attributes: Map<String, Any>
}
data class Subtype1(override val attributes: Map<String, Any> = mapOf("Some default for this type" to "Its value.")) : Example
data class Subtype2(override val attributes: Map<String, Any> = emptyMap()) : Example
data class Subtype3(override val attributes: Map<String, Any> = mapOf("Another default, but only for this subtype" to "Its own value.") : Example
I'd like to keep the data model immutable, so what I'm aiming for is generic addAttribute
and removeAttribute
methods that return the actual type of the receiver, not the base`Example` type and this is what I came up with:
inline fun <reified T : Example> T.addAttribute(key: String, value: Any): T = withAttributes(attributes + (key to value)) as T
inline fun <reified T : Example> T.removeAttribute(key: String): T = withAttributes(attributes - key) as T
fun Example.withAttributes(attributes: Map<String, Any>): Example {
return when (this) {
is Subtype1 -> copy(attributes = attributes)
is Subtype2 -> copy(attributes = attributes)
is Subtype3 -> copy(attributes = attributes)
}
}
I made these functions inline fun
so I can avoid marking each with @Suppress("UNCHECKED_CAST")
due to the cast to T
. A previous iteration dropped the inline
modifier, included the @Suppress
annotations, and had withAttributes
private instead of public but this is where I am currently.
Is there a more elegant way to do this? It feels a bit hacky even though it works. The safer approach would be to do Example.{add,remove}Attribute(...): Example
and put the onus on the caller to do any casting, but If I can be safe enough and keep Subtype1 -> Subtype1
, Subtype2 -> Subtype2
etc without writing methods for each subtype of Example
that would be cool.
Any insight is appreciated!ephemient
04/14/2023, 8:35 AMsealed interface Example<Self : Example> {
fun withAttributes(attributes: Map<String, Any>): Self
}
data class Subtype1 : Example<Subtype1> {
override fun withAttributes(attributes: Map<String, Any>) = copy(attributes = attributes)
}
ephemient
04/14/2023, 8:36 AMephemient
04/14/2023, 8:40 AMRyan Smith
04/15/2023, 7:57 PMExample<Self : Example>
is nifty, and I see what you mean about org.w3c.dom.Node
. Further reinforces that leaving casting up to the caller is the safer approach.
Thinking about it a little bit more, though, since T.{add,remove}Attribute
are marked inline
I'm effectively just providing a shortcut for the caller, no?
Once everything is compiled this:
val original: Subtype1 = Subtype1(mapOf())
val updated: Subtype1 = original.addAttribute("some key", "some value")
will get replaced with this:
val original: Subtype1 = Subtype1(mapOf())
val updated: Subtyp1 = original.withAttributes(original.attributes + ("some key" to "some value")) as T
because of inlining, right? So maybe this idea isn't so hacky after all maybe it's just reducing boilerplate.