Reputation: 811
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
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
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
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