Kotlin / KEEP

Kotlin Evolution and Enhancement Process
Apache License 2.0
3.3k stars 356 forks source link

Proposal: narrow type parameters in `when` expressions #296

Closed mjvmroz closed 2 years ago

mjvmroz commented 2 years ago

The typechecker will narrow values' types in the context of when expressions, which is a fantastic language feature. Would it be possible to extend this behaviour to type parameters?

Let's say I have a hierarchy like this:

sealed class ThingType<T : Thing<T>> {
    object One : ThingType<Thing.One>()
    object Two : ThingType<Thing.Two>()
}

sealed class Thing<Self : Thing<Self>> {
    data class One(val value: String) : Thing<One>()
    data class Two(val value: String) : Thing<Two>()
}

Encoding types as values this way allows for some cool stuff like:

interface ThingService {
    fun <T : Thing<T>> getThing(thingType: ThingType<T>): T

    fun demo() {
        // The typechecker is fine with this. In fact, it doesn't even
        // need the type annotation – it will infer Thing.One on its own.
        val thing: Thing.One = getThing(ThingType.One)
    }
}

Everything works great in type-land, but when it gets to the nitty-gritty world of values, things are a bit less pleasant. The typechecker has enough information to allow both of these functions:

fun <T : Thing<T>> thingType(thing: Thing<T>): ThingType<T> = when (thing) {
    is Thing.One -> ThingType.One
    is Thing.Two -> ThingType.Two
}

fun <T : Thing<T>> makeThing(type: ThingType<T>, value: String): T = when (type) {
    is ThingType.One -> Thing.One(value)
    is ThingType.Two -> Thing.Two(value)
}

But it doesn't. In each case for each function, I get type mismatch errors. For the first function, I get errors like this:

Type mismatch.
Required: ThingType<T>
Found:    ThingType.One

And for the second:

Type mismatch.
Required: T
Found:    Thing.One

Note that in the second case, the when conditions could be written interchangeably using either values or is, but the typechecker is upset either way.

My real-world use case for this today involves a slightly more complex case: I have a sealed identifier type for rows in a DynamoDB table, allowing ID values to inform my query generation, codec selection and return types. At the typelevel and on the consumer side, everything works great, but this limitation demands that I put casts in the implementation, which is a bit disappointing.

JakeWharton commented 2 years ago

This should be a YouTrack feature request not a KEEP. Please close and file at http://kotl.in/issue.

mjvmroz commented 2 years ago

Sure thing

mjvmroz commented 2 years ago

Raised here: https://youtrack.jetbrains.com/issue/KT-51782