4

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 }
broot
  • 21,588
  • 3
  • 30
  • 35
goshki
  • 120
  • 9
  • 1
    Kind of unrelated to what you are asking, but why is the type parameter `R` needed at all? – Sweeper Oct 15 '21 at 10:09
  • It is required to indicate that the `action` may return some result – this might be useful if you call conversion using reference to some existing method, i.e.: `source.convertTo(Result(), ::conversionAction)`. – goshki Oct 15 '21 at 10:12
  • 2
    Using `-> Unit` will make that work too. The return type will be ignored. – Sweeper Oct 15 '21 at 10:17
  • and maybe it's just me. It seems to me that defining this convertTo function doesn't do anything at all. Why define it if you're going to provide it the exact action? you can just call the action instead. It sort of feels like you use it to document your code that conversion is happening there or something. I don't see the added value but maybe I'm missing something. – Ivo Oct 15 '21 at 10:24
  • I very often miss such feature. Usually, it is possible to workaround by providing e.g. the return type or types of lambda params explicitly. Still, it would be nice to have, but its syntax may not be that trivial. – broot Oct 15 '21 at 10:26
  • As the discussion focused on your specific example and not on the problem/feature per se, I can provide other examples. What is the best way to do this on SO? Create some gist that you will look into and maybe copy to the question? Suggest edit to question? – broot Oct 15 '21 at 11:06
  • @broot, actually I have no idea what would be the best way to contribute examples on SO... but if you can create an edit suggestion with examples, I'll gladly accept it. – goshki Oct 15 '21 at 11:19
  • 1
    I added more examples. It took more space than I thought, but I guess the more is better in this case. Anyway, I believe this is not possible right now, so it would require language improvements. – broot Oct 15 '21 at 13:02
  • 1
    @IvoBeckers, I use `convertTo()` mainly as a syntactic sugar to introduce `this` in functions that mostly consist of copying from one object to another (making it possible to omit name of one variable throughout whole function block). It also let's you revert what's `this` and what's `it` in extension functions. Yes, I know it's somewhat dangerous to change meaning of `this` but suprisingly it can increase readability if used wisely. – goshki Oct 15 '21 at 13:31

2 Answers2

1

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.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Thank you for your input – it guided me to some additional conclusions. First of all, using `resultType: KClass` provides ability to use `KClass.createInstance()` making sure we find a parameterless constructor (if there's any). Secondly, after some additional tests, I found out that your remark about omitting parameter `R` is not applicable to all cases. Declaring `Unit` result is OK for lambdas but not for method references. I'll write my own answer to describe my solution in detail and explain how it expands on your answer. – goshki Oct 15 '21 at 12:30
1

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().

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> C.convertTo(receiver: T, action: T.(C) -> Unit) = 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> C.convertTo(resultType: KClass<T> = T::class, action: T.(C) -> Unit) = 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> C.convertTo(action: T.(C) -> Unit, 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()
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.

goshki
  • 120
  • 9
  • "Declaring Unit result for action is OK for lambdas but not for function references." [This is not true](https://pl.kotl.in/KkzlSelPX) Did you perhaps try it in an assignment context? It will work in an invocation context. – Sweeper Oct 15 '21 at 18:29
  • Yes, I believe assignment context is the whole point of using `convertTo()` function. Please, take a look at the code I've included in the answer above – if you'd change `T.(C) -> R` to `T.(C) -> Unit` in all three `convertTo()` versions then the line with `result3` would not compile. – goshki Oct 16 '21 at 08:46
  • 1
    I'm not talking about what context `convertTo` is used in. I'm talking about the context in which the function reference is used in. That said, Anyway, [here's](https://pl.kotl.in/W7GrN3TvW) a demo to show that the code compiles. Note that the code won't run on the playground because reflection isn't available there. – Sweeper Oct 16 '21 at 09:02
  • @Sweeper, thanks again for your input – I've retried the above code with `T.(C) -> Unit` and it does indeed run OK without `R`. I'll update the code in my answer. – goshki Oct 16 '21 at 13:28