Answer for the simplified example
(answer for the first example below)
It looks like you don't care about the precise type parameter of the X[_] in case class Y(xs: X[_]*), as long as they are all the same. You just want to prevent users to create Ys that don't respect this.
One way to achive this would be to make the default Y constructor private:
case class Y private (xs: Seq[X[_]])
// ^^^^^^^ makes the default constructor private to Y, xs is still public
// Note also that xs is now a Seq, we will recover the repeated arg list below.
and define your own constructor this way:
object Y {
def apply[B](): Y = Y(Nil)
def apply[B](x0: X[B], xs: X[B]*): Y = Y(x0 +: xs)
// Note that this is equivalent to
// def apply[B](xs: X[B]*): Y = Y(xs)
// but the latter conflicts with the default (now private) constructor
}
Now one can write
Y()
Y(X("a"))
Y(X(1), X(1), X(5), X(6))
Y[Int](X(1), X(1), X(5), X(6))
and the following doesn't compile:
Y(X(1), X("1"))
Answer to the first example
We make the constructor private and change the repeated arg list to a Seq as above:
case class Rect2D[A] private (rows: Seq[Sized[Seq[A], _]])
// ^^^^^^^ ^^^^ ^
Let's define our own constructor(s):
object Rect2D {
def apply[A](): Rect2D[A] = Rect2D[A](Nil)
def apply[A,N <: Nat](r0: Sized[Seq[A], N], rs: Sized[Seq[A], N]*): Rect2D[A] = Rect2D[A](r0 +: rs)
}
Now the following compiles:
val r0: Rect2D[_] = Rect2D()
val r: Rect2D[Int] = Rect2D[Int]()
val r1: Rect2D[Int] = Rect2D(Sized[Seq](1, 2))
val r2: Rect2D[Int] = Rect2D(Sized[Seq](1, 2), Sized[Seq](2, 3))
val r3: Rect2D[Int] = Rect2D(Sized[Seq](1, 2), Sized[Seq](2, 3), Sized[Seq](2, 3), Sized[Seq](2, 3))
val r4: Rect2D[Any] = Rect2D(Sized[Seq](1, 2), Sized[Seq]("a", "b"), Sized[Seq](2, 3), Sized[Seq](2, 3)) // Works because both Sized and Seq are covariant
// Types added as a check, they can be removed
and the following doesn't:
val r5 = Rect2D(Sized[Seq](1, 2), Sized[Seq](1, 2, 3))
One drawback is that one cannot write something like
val r2 = Rect2D[Int](Sized[Seq](1, 2), Sized[Seq](2, 3))
// ^^^^^
one has to write this instead
val r2 = Rect2D[Int, Nat._2](Sized[Seq](1, 2), Sized[Seq](2, 3))
// ^^^^^^^^
Let's fix this!
Enhanced solution for the first example
A cleaner solution would be define the constructors above this way:
object Rect2D {
def apply[A,N <: Nat](r0: Sized[Seq[A], N], rs: Sized[Seq[A], N]*): Rect2D[A] = Rect2D[A](r0 +: rs) // Same as above
case class Rect2DBuilder[A]() {
def apply(): Rect2D[A] = Rect2D[A](Nil)
def apply[N <: Nat](r0: Sized[Seq[A], N], rs: Sized[Seq[A], N]*): Rect2D[A] = Rect2D[A](r0 +: rs)
}
def apply[A] = new Rect2DBuilder[A]
}
Now we can also write
val r2 = Rect2D[Int](Sized[Seq](1, 2), Sized[Seq](2, 3))
and the following would not compile
val r4 = Rect2D[Int](Sized[Seq](1, 2), Sized[Seq]("a", "b"), Sized[Seq](2, 3), Sized[Seq](2, 3))
// ^^^^^ ^^^^^^^^