Empresas
Empregos
  • Sobre nós
  • Soluções
    • Publicação de vagas
      Publique sua vaga e receba candidatos qualificados em 48h.
    • Avaliações de candidatos
      Mais de 500 testes técnicos e psicológicos, mais anti-fraude.
    • Headhunting
      Busca executiva personalizada do início ao fim.
    • Folha de Pagamento + EOR
      Dispersão de folha e EOR em mais de 15 países da LATAM.
  • Preços
  • Empregos

0

202
Visualizações
Compile error with Kotlin generics and sealed interface

I have a question regarding Kotlin and generics. I have a compile error, that I don't understand. I have a very simplified example below. Since Kotlin data classes can't inherit from other data classes, I've done a lot of my class design with composition. That leads to nested data classes. At some points I use generics to switch the type for parameters I can pass into an object. I need to modify values of these data classes and because they are immutable, I use the copy function and put that stuff into a Mutator class.

class Mutator<out P : Params>(val form: Form<P>) {
    fun modifyName(name: String): Form<P> =
        when (form.params) {
            is DefaultParams -> form.copy(params = form.params.copy(name = name)) // compile error
            is ExtendedParams -> form.copy(params = form.params.copy(name = name)) // compile error
            else -> form // also.. why do I need that.. Params is sealed..
        }
}

sealed interface Params

data class DefaultParams(val name: String) : Params
data class ExtendedParams(val name: String, val age: Int) : Params

data class Form<out P : Params>(val params: P)

fun main() {
    val form: Form<DefaultParams> = Form(DefaultParams("John"))
    val mutator: Mutator<DefaultParams> = Mutator(form)
    val newForm: Form<DefaultParams> = mutator.modifyName("Joe")

    val next_form: Form<ExtendedParams> = Form(ExtendedParams("John", 30))
    val next_mutator: Mutator<ExtendedParams> = Mutator(next_form)
    val next_newForm: Form<ExtendedParams> = next_mutator.modifyName("Joe")
}

I get errors on the first two branches of the when block.

Type mismatch. Required: P Found: DefaultParams

Type mismatch. Required: P Found: ExtendedParams

Shouldn't Params be the upper bound of P and thus fine to find DefaultParams or ExtendedParams? And also Params should be sealed, but I need an else block in the when.

over 4 years ago · Santiago Trujillo
3 Respostas
Responde à pergunta

0

I would suggest that you just use an unchecked cast here:

is DefaultParams -> form.copy(params = form.params.copy(name = name) as P)
is ExtendedParams -> form.copy(params = form.params.copy(name = name) as P)

Note that this unchecked cast is very safe, because of the semantics of the copy method. copy does not change the runtime type of its receiver. Therefore, it is no more unsafe to pass the copied form.params than to pass the original form.params. And the compiler allows you to pass the original form.params here. The compiler is just not smart enough to work out that since form.params is a specific Params, P must be that specific Params too.

The when statement is not exhaustive because the type of form.params is not Params, but P. (Yes, the compiler is quite stupid sometimes). According to the spec, a condition for an exhaustive when expression is:

The bound expression is of a sealed class or interface

Note that it doesn't say "a type parameter bounded to a sealed class or interface". So if you just add as Params, it will become exhaustive:

fun modifyName(name: String): Form<P> =
    (form.params as Params).let { formParams ->
        when (formParams) {
            is DefaultParams -> form.copy(params = formParams.copy(name = name) as P)
            is ExtendedParams -> form.copy(params = formParams.copy(name = name) as P)
        }
    }
over 4 years ago · Santiago Trujillo Relatório

0

I'm not sure exactly myself why it is the case, but I think it's something to do with P being a generic. Because if you rewrite the Mutator as this you can leave out the else and there's also no error.

class Mutator(val form: Form<Params>) {
    fun modifyName(name: String): Form<Params> =
        when (form.params) {
            is DefaultParams -> form.copy(params = form.params.copy(name = name)) // compile error
            is ExtendedParams -> form.copy(params = form.params.copy(name = name)) // compile error
        }
}

In fact, I think for your use case you don't need generics and this solution might be exactly what you need.

EDIT:

if you modify it to this

class Mutator<out P : Params>(val form: Form<P>) {
    fun modifyName(name: String): Form<P> =
        when (form.params) {
            is DefaultParams -> form.copy(params = (form.params.copy(name = name) as P))
            is ExtendedParams -> form.copy(params = (form.params.copy(name = name) as P))
            else -> form
        }
}

it should work like you wanted. It will no longer have a compiler error. It will give a warning though that as P is an unchecked cast. But for us it's easy to see that this never will be a problem. I guess the compiler is just not smart enough.

over 4 years ago · Santiago Trujillo Relatório

0

You have to cast the parameters to P because that is what the compiler expects for the copy function. However, this gives you a warning about an unchecked cast exception (which is relatively safe, see Sweeper's answer).

In case you'd like to avoid this Exception, you can turn the modifyName into inline function instead and use reified parameters (see the Kotlin docs on unchecked casts, and this excellent answer on the reified keyword).

inline fun<reified P: Params> Form<P>.modifyName(name: String): Form<P> =
    when (this.params) {
        is DefaultParams -> copy(params = params.copy(name = name) as P)
        is ExtendedParams -> copy(params = params.copy(name = name) as P)
        else -> this
    }

sealed interface Params

data class DefaultParams(val name: String) : Params
data class ExtendedParams(val name: String, val age: Int) : Params

data class Form<out P : Params>(val params: P)

fun main() {
    val form: Form<DefaultParams> = Form(DefaultParams("John"))
    val newForm: Form<DefaultParams> = form.modifyName("Joe")

    val nextForm: Form<ExtendedParams> = Form(ExtendedParams("John", 30))
    val nextNewForm: Form<ExtendedParams> = nextForm.modifyName("Joe")
}

This will inline the types in the generated bytecode, so that when you call form.modifyName("Joe") in the above example, the function modifyName will know that P is DefaultParams, and thus you can return an instance of Form<DefaultParams> (instead of Form<Params>).

over 4 years ago · Santiago Trujillo Relatório
Responde à pergunta
Encontrar trabalhos remotos

Descubra a nova forma de encontrar um emprego!

melhores empregos
Principais categorias de trabalho
Empresas
Postar vaga Preços Comercial
Jurídico
Termos e Condições Política de privacidade
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomende algumas ofertas para mim
Preciso de ajuda