Hi everyone, I have a sealed type that I'm workin...
# stdlib
r
Hi everyone, I have a sealed type that I'm working with that maintains a mapping of extra attributes that in the context of my application will be subject to create/update/delete operations by a user through a form interface and each subtype has their own default attributes that aren't proper properties in this case because those are subject to removal by the user as well. More in 🧵.
The hierarchy looks something like this:
Copy code
sealed 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:
Copy code
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!
e
Copy code
sealed 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)
}
that won't work for deeper hierarchies though
for comparison, https://docs.oracle.com/javase/8/docs/api/org/w3c/dom/Node.html just leaves all the casting up the the caller
r
Hmm
Example<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:
Copy code
val original: Subtype1 = Subtype1(mapOf())
val updated: Subtype1 = original.addAttribute("some key", "some value")
will get replaced with this:
Copy code
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.