Home > Software design >  Providing only one type parameter to an extension function with multiple type parameters in Kotlin
Providing only one type parameter to an extension function with multiple type parameters in Kotlin

Time:10-15

Introduction

In Kotlin I have a generic conversion extension function that simplifies conversion of this object of type C to an object of another type T (declared as the receiver) with additional conversion action that treats receiver as this and also provides access to original object:

inline fun <C, T, R> C.convertTo(receiver: T, action: T.(C) -> R) = receiver.apply {
    action(this@convertTo)
}

It is used like this:

val source: Source = Source()
val result = source.convertTo(Result()) {
    resultValue = it.sourceValue
    // and so on...
}

I noticed I often use this function on receivers that are created by parameterless constructors and thought it would be nice to simplify it even more by creating additional version of convertTo() that automates construction of the receiver based on its type, like this:

inline fun <reified T, C, R> C.convertTo(action: T.(C) -> R) = with(T::class.constructors.first().call()) {
    convertTo(this, action) // calling the first version of convertTo()
}

Unfortunately, I cannot call it like this:

source.convertTo<Result>() {}

because Kotlin expects three type parameters provided.

Question

Given above context, is it possible in Kotlin to create a generic function with multiple type parameters that accepts providing just one type parameter while other types are determined from the call-site?

Additional examples (by @broot)

Imagine there is no filterIsInstance() in stdlib and we would like to implement it (or we are the developer of stdlib). Assume we have access to @Exact as this is important for our example. It would be probably the best to declare it as:

inline fun <T, reified V : T> Iterable<@Exact T>.filterTyped(): List<V>

Now, it would be most convenient to use it like this:

val dogs = animals.filterTyped<Dog>() // compile error

Unfortunately, we have to use one of workarounds:

val dogs = animals.filterTyped<Animal, Dog>()
val dogs: List<Dog> = animals.filterTyped()

The last one isn't that bad.

Now, we would like to create a function that looks for items of a specific type and maps them:

inline fun <T, reified V : T, R> Iterable<T>.filterTypedAndMap(transform: (V) -> R): List<R>

Again, it would be nice to use it just like this:

animals.filterTypedAndMap<Dog> { it.barkingVolume } // compile error

Instead, we have this:

animals.filterTypedAndMap<Animal, Dog, Int> { it.barkingVolume }
animals.filterTypedAndMap { dog: Dog -> dog.barkingVolume }

This is still not that bad, but the example is intentionally relatively simple to make it easy to understand. In reality the function would be more complicated, would have more typed params, lambda would receive more arguments, etc. and then it would become hard to use. After receiving the error about type inference, the user would have to read the definition of the function thoroughly to understand, what is missing and where to provide explicit types.

As a side note: isn't it strange that Kotlin disallows code like this: cat is Dog, but allows this: cats.filterIsInstance<Dog>()? Our own filterTyped() would not allow this. So maybe (but just maybe), filterIsInstance() was designed like this exactly because of the problem described in this question (it uses * instead of additional T).

Another example, utilizing already existing reduce() function. We have function like this:

operator fun Animal.plus(other: Animal): Animal

(Don't ask, it doesn't make sense)

Now, reducing a list of dogs seems pretty straightforward:

dogs.reduce { acc, item -> acc   item } // compile error

Unfortunately, this is not possible, because compiler does not know how to properly infer S to Animal. We can't easily provide S only and even providing the return type does not help here:

val animal: Animal = dogs.reduce { acc, item -> acc   item } // compile error

We need to use some awkward workarounds:

dogs.reduce<Animal, Dog> { acc, item -> acc   item }
(dogs as List<Animal>).reduce { acc, item -> acc   item }
dogs.reduce { acc: Animal, item: Animal -> acc   item }

CodePudding user response:

The type parameter R is not necessary:

inline fun <C, T> C.convertTo(receiver: T, action: T.(C) -> Unit) = receiver.apply {
    action(this@convertTo)
}
inline fun <reified T, C> C.convertTo(action: T.(C) -> Unit) = with(T::class.constructors.first().call()) {
    convertTo(this, action) // calling the first version of convertTo()
}

If you use Unit, even if the function passed in has a non-Unit return type, the compiler still allows you to pass that function.

And there are other ways to help the compiler infer the type parameters, not only by directly specifying them in <>. You can also annotate the variable's result type:

val result: Result = source.convertTo { ... }

You can also change the name of convertTo to something like convert to make it more readable.

Another option is:

inline fun <T: Any, C> C.convertTo(resultType: KClass<T>, action: T.(C) -> Unit) = with(resultType.constructors.first().call()) {
    convertTo(this, action)
}

val result = source.convertTo(Result::class) { ... }

However, this will conflict with the first overload. So you have to resolve it somehow. You can rename the first overload, but I can't think of any good names off the top of my head. I would suggest that you specify the parameter name like this

source.convertTo(resultType = Result::class) { ... }

Side note: I'm not sure if the parameterless constructor is always the first in the constructors list. I suggest that you actually find the parameterless constructor.

CodePudding user response:

This answer does not solve the stated problem but incorporates input from @Sweeper to provide a workaround at least simplifying result object instantiation.

First of all, the main stated problem can be somewhat mitigated if we explicitly state variable's result type (i.e. val result: Result = source.convertTo {}) but it's not enough to solve the problem in cases described by @broot.

Secondly, using KClass<T> as result parameter type provides ability to use KClass<T>.createInstance() making sure we find a parameterless constructor (if there's any – if there is none, then result-instantiating convertTo() is not eligible for use). We can also benefit from Kotlin's default parameter values to make result parameter type omittable from calls, we just need to take into account that action might be provided as lambda (last parameter of call) or function reference – this will require two versions of result-instantiating convertTo().

And thirdly, omitting parameter R specifying result of action is not applicable to all cases. Declaring Unit result for action is OK for lambdas but not for function references.

So, taking all the above into account, I've come up with this implementation(s) of convertTo():

// version A: basic, expects explicitly provided instance of `receiver`
inline fun <C, T, R> C.convertTo(receiver: T, action: T.(C) -> R) = receiver.apply {
    action(this@convertTo)
}

// version B: can instantiate result of type `T`, supports calls where `action` is a last lambda
inline fun <C, reified T : Any, R> C.convertTo(resultType: KClass<T> = T::class, action: T.(C) -> R) = with(resultType.createInstance()) {
    (this@convertTo).convertTo(this@with, action)
}

// version C: can instantiate result of type `T`, supports calls where `action` is passed by reference
inline fun <C, reified T : Any, R> C.convertTo(action: T.(C) -> R, resultType: KClass<T> = T::class) = with(resultType.createInstance()) {
    (this@convertTo).convertTo(T::class, action)
}

All three versions work together depending on a specific use case. Below is a set of examples explaining what version is used in what case.

class Source { var sourceId = "" }
class Result { var resultId = "" }

val source = Source()

fun convertX(result: Result, source: Source) {
    result.resultId = source.sourceId
}

fun convertY(result: Result, source: Source) = true

fun Source.toResultX(): Result = convertTo { resultId = it.sourceId  }

fun Source.toResultY(): Result = convertTo(::convertX)

val result0 = source.convertTo(Result()) { resultId = it.sourceId } // uses version A of convertTo()
val result1: Result = source.convertTo { resultId = it.sourceId } // uses version B of convertTo()
val result2: Result = source.convertTo(::convertX) // uses version C of convertTo()
val result3: Result = source.convertTo(::convertY) // uses version C of convertTo(), would fail if `action` was declared with `Unit` return type
val result4: Result = source.toResultX() // uses version B of convertTo()
val result5: Result = source.toResultY() // uses version C of convertTo()

P.S.: As @Sweeper notices, convertTo might not be a good name for the result-instantiating versions (as it's not as readable as with basic version) but that's a secondary problem.

  • Related