Omitting only some type arguments from generic functions

When discussing calling generic functions Generics: in, out, where | Kotlin Documentation says “Type arguments can be omitted if they can be inferred from the context”.

Is that only the case if all type arguments can be inferred, or is it the case that if you need to provide one type argument you must provide all of them (even if some of them are provided as _)?

My motivation for this is as follows:

I’m trying to make some code a bit more ergonomic.

I’m modelling data using GitHub - michaelbull/kotlin-result at 1.1.21. This is a Result<V, E> monad, where V is the “success” type, E is the error type.

For most data I have a sealed interface to represent states “data is loading” and “data is loaded”, and classes for errors, e.g.

sealed interface Data {
    data object Loading : Data
    data class Loaded(val data: String) : Data  /* String for demo purposes */
}

sealed interface AppError {
    data class NetworkError(...)
    data class JsonError(...)
    // etc
}

fun someApiCall(...) : Result<Data, AppError>

Kotlin-Result provides a map inline function that operates over the Result type, applying a function to the value if it’s Ok, or returning the error unchanged. I generally only need to operate on the contained data if the sub-type is Data.Loaded, so this leads to code like this:

val newData = data.map {
    when (it) {
        is Data.Loading -> it,
        is Data.Loaded -> { /* transform the data in some way */ }
    }
}

I thought a more ergonomic approach would be to be able to write something like:

val newData = data.mapIfInstance<Data.Loaded> { /* transform the data in some way */ }

and wrote this to do so:

public inline infix fun <V, E, reified T : V> Result<V, E>.mapIfInstance(transform: (T) -> V): Result<V, E> {
    return when (this) {
        is Ok -> (value as? T)?.let { Ok(transform(it)) } ?: this
        is Err -> this
    }
}

However, if I call this with just the type parameter T, e.g:

val newData = data.mapIfInstance<Data.Loaded> { /* transform */ }

I get:

Inapplicable candidate(s): fun <V, E, reified T : V> Result<V, E>.mapIfInstance(transform: (T) -> V): Result<V, E>

If I call it with all three type parameters it works:

val newData = data.mapIfInstance<Data, AppError, Data.Loaded> { /* transform */ }

I’ve made a few different changes (e.g., to the order of the type paramters in the function declaration) to see if I can get the compiler to infer the missing types V and E when I only provide T, but with no success.

Playground code that shows the problem at Kotlin Playground: Edit, Run, Share Kotlin Code Online

Is what I want to achieve possible?

Did you scroll down to the last section of the Kotlin docs you linked? That’s headed Underscore operator for type arguments, and shows how you can use _ in place of any type argument. With that, you can specify some type arguments but not others, in any combination.

(This feature was added in Kotlin 1.7, two years ago.)

Thanks for the suggestion. I’d tried that, and while it does reduce the amount of information that needs to be provided, you still need to provide all three type arguments (even if two of them are _).

So:

val newData = data.mapIfInstance<_, _, Data.Loaded> { /* transform */ }

I’m hoping there’s an approach that lets me only supply the one type argument that can’t be inferred, so the call site looks like:

val newData = data.mapIfInstance<Data.Loaded> { /* transform */ }

I’ve edited the second paragraph of the original question to make this clearer.

You could use this TypeWrapper approach:

public inline fun <V, E, reified T : V> Result<V, E>.mapIfInstance(type: TypeWrapper<T> = type(), transform: (T) -> V): Result<V, E> {
    return when (this) {
        is Ok -> (value as? T)?.let { Ok(transform(it)) } ?: this
        is Err -> this
    }
}

sealed interface TypeWrapper<out T> {
    object IMPL: TypeWrapper<Nothing>
}

fun <T> type(): TypeWrapper<T> = TypeWrapper.IMPL
//Usage
fun main() {
	println(y.mapIfInstance(type<Data.Loaded>()) { Data.Loaded("world") })
}

Or you could also help the compiler infer the type by specifying it on the lambda:

println(x.mapIfInstance { _: Data.Loaded -> Data.Loaded("world")})
2 Likes

Oh, that’s great, thanks. About as ergonomic as it can get I think.

1 Like

Related issue: https://youtrack.jetbrains.com/issue/KT-1215/Support-default-values-for-generic-function-and-classes-type-parameters

1 Like