Alexey Stepanov
Alexey Stepanov

Reputation: 811

How can I use vararg with different generics in Kotlin?

I want to use vararg with generics with different types of each arguments

what I've already tried:

class GeneralSpecification<T> {
    fun <P> ifNotNullCreateSpec(vararg propToCreateFun: Pair<P?, (P) -> Specification<T>>): List<Specification<T>> = 
       propToCreateFun.mapNotNull { (prop, funCreateSpec) ->
            prop?.let(funCreateSpec)
    }
...
}

but I can't use this like:

ifNotNullCreateSpec("asdf" to ::createStringSpec, 5 to ::createIntSpec)

(different types in vararg pairs)

How can I use vararg with different generics, when I need to restrict types in vararg? (pair.first type depends on pair.second type)

Upvotes: 3

Views: 6033

Answers (3)

George Leung
George Leung

Reputation: 1552

Adding a new answer after 3 years, as the two existing answers feel incomplete. One describes why it fails without a solution. The other provides a solution without much theory.


fun <P> ifNotNullCreateSpec(vararg propToCreateFun: Pair<P?, (P) -> Specification<T>>)

Given this method signature, what is P? For one pair it's String, for another it's Int. But we can only supply 1 P as the type parameter of the method.

Can we have P as Any? For the first elements in the two pairs, the type checks out, Int and String are both subtype of Any?

But for the second element, is (String) -> Specification<T> a subtype of (Any) -> Specification<T>? The answer is no. In fact the opposite is true.

(Any) -> ... is a subtype of (String) -> ....
If you replace the parameter's type with a supertype, the function type is a subtype. This is said to be contravariance.


So we cannot have one single P for the different pairs. I believe the asker already knew this, and was asking how to link up the types inside the pairs - but independent between the pairs.*


Feel free to skip this section which goes full type theory.

This pattern is called existential types**. The idea is that "there exist" a type which links the two inner objects together, but the user of the composed object doesn't care. Funny enough, the example given in Wikipedia is exactly the same as the current question.

"T = ∃X { a: X; f: (X → int); }"

The type T has a thing of type X, and a function that takes X. And when objects of type T are used, I don't care about what X is.


To express this pattern in Kotlin, the accepted answer creates a new class WithSpec<P, T> to link up the P type between prop and funCreateSpec, then uses star projection to ignore the type P in ifNotNullCreateSpec.


*That makes the current upvote count quite perplexing.

**Scala used to support existential types with the keyword forSome, but it was way too fiddly. It was dropped in Scala 3.

Upvotes: 1

Alexey Romanov
Alexey Romanov

Reputation: 170723

Instead of using Pair, consider defining your own type:

class WithSpec<P, T>(val prop: P?, val funCreateSpec: (P) -> Specification<T>) {
    fun spec() = prop?.let(funCreateSpec)
}

Why? Because it allows you to do

class GeneralSpecification<T> {
    fun ifNotNullCreateSpec(vararg propToCreateFun: WithSpec<*, T>): List<Specification<T>> = 
       propToCreateFun.mapNotNull { it.spec() }
    ...
}

ifNotNullCreateSpec(WithSpec("asdf", ::createStringSpec), WithSpec(5, ::createIntSpec))

You can easily add a to-like extension function returning WithSpec if you want to get even closer to your original code.

See https://kotlinlang.org/docs/reference/generics.html#star-projections if you don't know what * means.

Upvotes: 1

TheOperator
TheOperator

Reputation: 6456

If you want to store the different functions together, you need to treat the parameter type T with out variance. This means T is only used in the output of the class. In practice, this means that Conversions of Spec<Derived> -> Spec<Base> are allowed, if Derived extends/implements Base.

Without such a constraint, the function types are not related, and as such you cannot store them in a common array (varargs are just syntactic sugar for array parameters).

Example:

class Spec<out T>

fun createStringSpec() = Spec<String>()
fun createIntSpec() = Spec<Int>()

fun <T> ifNotNullCreateSpec(vararg pairs: Pair<T, () -> Spec<T>>) = Unit

fun main() {
    ifNotNullCreateSpec("asdf" to ::createStringSpec, 5 to ::createIntSpec)
}

With a parameter T such as in (T) -> Spec<T>, the T type also appears in the input of the function type. This means that you can no longer store the function types together, because they take parameters of different types -- with which type would you invoke such a function?

What you would need to do here is to find the most common denominator. One example is to accept a Any parameter and do a runtime check/dispatch for the actual type.

See also my recent answer here: https://stackoverflow.com/a/55572849

Upvotes: 3

Related Questions