I have a generic sealed class, used to represent either single values or pairs of values (split before and after a certain event):
sealed class Splittable<T>
data class Single<T>(val single: T) : Splittable<T>()
data class Split<T>(val before: T,
val after : T) : Splittable<T>()
I would like to define data classes that are generic (parameterizable) over Splittable
, so that the properties of the class must either be all Single or all Split. I thought this would do it:
data class Totals<SInt : Splittable<Int>>(
val people : SInt,
val things : SInt
)
val t1 = Totals(
people = Single(3),
things = Single(4)
)
val t2 = Totals(
people = Split(3, 30),
things = Split(4, 40)
)
But I was wrong, because it allows invalid combinations:
val WRONG = Totals(
people = Single(3),
things = Split(4, 40)
)
Moreover, what if my class has more than one basic type, for instance both Int
and Boolean
? How do I write the generic signature?
data class TotalsMore<S : Splittable>(
val people : S<Int>,
val things : S<Int>,
val happy : S<Boolean>
)
val m1 = TotalsMore(
people = Single(3),
things = Single(4),
happy = Single(true)
)
val m2 = TotalsMore(
people = Split(3, 30),
things = Split(4, 40),
happy = Split(true, false)
)
The data class declaration gives errors:
error: one type argument expected for class Splittable<T>
data class TotalsMore<S : Splittable>(
^
error: type arguments are not allowed for type parameters
val people : S<Int>,
^
error: type arguments are not allowed for type parameters
val things : S<Int>,
^
error: type arguments are not allowed for type parameters
val happy : S<Boolean>
^
So it appears I cannot pass a higher-kinded type as a type parameter. Bummer.
I can decouple the two generics:
data class TotalsMore<SInt : Splittable<Int>,
SBoolean: Splittable<Boolean>>(
val people : SInt,
val things : SInt,
val happy : SBoolean
)
This works, but it makes it even more obvious that you can mix and match Single and Split, which I want to forbid:
val WRONG = TotalsMore(
people = Single(3),
things = Single(4),
happy = Split(true, false)
)
I would like every object of those data classes to either be made of all Single values, or all Split values, not a mix and match.
Can I express it using Kotlin's types?
Your Totals
class requires a generic type parameter, but you don't specify one in the constuctor your example. The way that works is with type inference: the Kotlin compiler figures the generic type out from the other parameters. The reason you can mix Single
and Split
in your first WRONG
example is that the compiler sees the two arguments and infers the common supertype from them. So you're actually constructing a Totals<Splittable<Int>>
.
If you'd specify a subtype explicitly, you wouldn't be able to mix:
val WRONG = Totals<Single<Int>>(
people = Single(3),
things = Split(4, 40) /** Type inference failed. Expected type mismatch: inferred type is Split<Int> but Single<Int> was expected */
)
So what you want to do is to accept the subclasses of Splittable
but not Splittable
itself as a generic parameter.
You can achieve that with an additional interface for your subclasses and an additional generic constraint with a where
-clause:
sealed class Splittable<T>
interface ConcreteSplittable
data class Single<T>(val single: T) : Splittable<T>(), ConcreteSplittable
data class Split<T>(val before: T,
val after : T) : Splittable<T>(), ConcreteSplittable
data class Totals<SInt : Splittable<Int>>(
val people : SInt,
val things : SInt
) where SInt : ConcreteSplittable
val t1 = Totals<Single<Int>>(
people = Single(3),
things = Single(4)
)
val t2 = Totals(
people = Split(3, 30),
things = Split(4, 40)
)
val WRONG = Totals( /** Type parameter bound for SInt in constructor Totals<SInt : Splittable<Int>>(people: SInt, things: SInt) where SInt : ConcreteSplittable is not satisfied: inferred type Any is not a subtype of Splittable<Int> */
people = Single(3),
things = Split(4, 40)
)
As for the second part, I don't think that's quite possible. As you noted, type arguments are not allowed for type parameters.
Unfortunately, you can also not introduce a third type parameter S
and restrict SInt
and SBool
to both the common type S
and to Splittable<Int>
or Splittable<Bool>
respectively.
data class TotalsMore<S, SInt, SBool>
(
val people : SInt,
val things : SInt,
val happy : SBool
) where S : ConcreteSplittable,
SInt : S,
SInt : Splittable<Int>, /** Type parameter cannot have any other bounds if it's bounded by another type parameter */
SBool : S,
SBool : Splittable<Boolean> /** Type parameter cannot have any other bounds if it's bounded by another type parameter */
What you could do, is to create "safe" type aliases, like this:
data class TotalsMore<SInt : Splittable<Int>, SBool : Splittable<Boolean>> (
val people : SInt,
val things : SInt,
val happy : SBool )
typealias SingleTotalsMore = TotalsMore<Single<Int>, Single<Boolean>>
typealias SplitTotalsMore = TotalsMore<Split<Int>, Split<Boolean>>
val s = SingleTotalsMore(
people = Single(3),
things = Single(4),
happy = Single(true) )
Creating a mixed TotalsMore
would still be possible though.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With